dynflow 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
 - data/Gemfile +9 -0
 - data/MIT-LICENSE +20 -0
 - data/README.md +151 -0
 - data/Rakefile +7 -0
 - data/dynflow.gemspec +23 -0
 - data/examples/events.rb +71 -0
 - data/examples/workflow.rb +140 -0
 - data/lib/dynflow.rb +11 -0
 - data/lib/dynflow/action.rb +121 -0
 - data/lib/dynflow/bus.rb +56 -0
 - data/lib/dynflow/dispatcher.rb +36 -0
 - data/lib/dynflow/logger.rb +34 -0
 - data/lib/dynflow/message.rb +38 -0
 - data/lib/dynflow/orch_request.rb +14 -0
 - data/lib/dynflow/orch_response.rb +5 -0
 - data/lib/dynflow/version.rb +3 -0
 - data/test/action_test.rb +24 -0
 - data/test/bus_test.rb +55 -0
 - data/test/dispatcher_test.rb +108 -0
 - data/test/test_helper.rb +83 -0
 - metadata +135 -0
 
    
        data/.gitignore
    ADDED
    
    | 
         @@ -0,0 +1 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            Gemfile.lock
         
     | 
    
        data/Gemfile
    ADDED
    
    
    
        data/MIT-LICENSE
    ADDED
    
    | 
         @@ -0,0 +1,20 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            Copyright 2012 Pavel Pokorný
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Permission is hereby granted, free of charge, to any person obtaining
         
     | 
| 
      
 4 
     | 
    
         
            +
            a copy of this software and associated documentation files (the
         
     | 
| 
      
 5 
     | 
    
         
            +
            "Software"), to deal in the Software without restriction, including
         
     | 
| 
      
 6 
     | 
    
         
            +
            without limitation the rights to use, copy, modify, merge, publish,
         
     | 
| 
      
 7 
     | 
    
         
            +
            distribute, sublicense, and/or sell copies of the Software, and to
         
     | 
| 
      
 8 
     | 
    
         
            +
            permit persons to whom the Software is furnished to do so, subject to
         
     | 
| 
      
 9 
     | 
    
         
            +
            the following conditions:
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            The above copyright notice and this permission notice shall be
         
     | 
| 
      
 12 
     | 
    
         
            +
            included in all copies or substantial portions of the Software.
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
         
     | 
| 
      
 15 
     | 
    
         
            +
            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         
     | 
| 
      
 16 
     | 
    
         
            +
            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
         
     | 
| 
      
 17 
     | 
    
         
            +
            NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
         
     | 
| 
      
 18 
     | 
    
         
            +
            LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
         
     | 
| 
      
 19 
     | 
    
         
            +
            OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
         
     | 
| 
      
 20 
     | 
    
         
            +
            WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         
     | 
    
        data/README.md
    ADDED
    
    | 
         @@ -0,0 +1,151 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            DYNamic workFLOW
         
     | 
| 
      
 2 
     | 
    
         
            +
            ================
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            In traditional workflow engines, you specify a static workflow and
         
     | 
| 
      
 5 
     | 
    
         
            +
            then run it with various inputs. Dynflow takes different approach.
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            You specify the inputs and the workflow is generated on the fly. You
         
     | 
| 
      
 8 
     | 
    
         
            +
            can either specify the steps explicitly or subscribe one action to
         
     | 
| 
      
 9 
     | 
    
         
            +
            another. This is suitable for plugin architecture, where you can't
         
     | 
| 
      
 10 
     | 
    
         
            +
            write the whole process on one place.
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            Dynflow doesn't differentiate between workflow and action. Instead,
         
     | 
| 
      
 13 
     | 
    
         
            +
            every action can populate another actions, effectively producing the
         
     | 
| 
      
 14 
     | 
    
         
            +
            resulting set of steps.
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            The whole execution is done in three phases:
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            1. *Planning phase*
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
              Construct the execution plan for the workflow. It's invoked by
         
     | 
| 
      
 21 
     | 
    
         
            +
              calling `trigger` on an action. Two mechanisms are used to get the set
         
     | 
| 
      
 22 
     | 
    
         
            +
              of actions to be executed:
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                a. explicit calls of `plan_action` methods in the `plan` method
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                b. implicit associations: an action A subscribes to an action B,
         
     | 
| 
      
 27 
     | 
    
         
            +
                which means that the action A is executed whenever the action B
         
     | 
| 
      
 28 
     | 
    
         
            +
                occurs.
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            The output of this phase is a set of actions and their inputs.
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            2. *Execution phase*
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
              The plan is being executed step by step, calling the run method of
         
     | 
| 
      
 35 
     | 
    
         
            +
              an action with corresponding input. The results of every action are
         
     | 
| 
      
 36 
     | 
    
         
            +
              written into output attribute.
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
              The run method should be stateless, with all the needed information
         
     | 
| 
      
 39 
     | 
    
         
            +
              included in the input from planning phase. This allows us to
         
     | 
| 
      
 40 
     | 
    
         
            +
              control the workflow execution: the state of every action can be
         
     | 
| 
      
 41 
     | 
    
         
            +
              serialized therefore the workflow itself can be persisted. This makes
         
     | 
| 
      
 42 
     | 
    
         
            +
              it easy to recover from failed actions by rerunning it.
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
            3. *Finalization phase*
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
              Take the results from the execution phase and perform some additional
         
     | 
| 
      
 47 
     | 
    
         
            +
              tasks. This is suitable for example for recording the results into
         
     | 
| 
      
 48 
     | 
    
         
            +
              database.
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            Every action can participate in every phase.
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
            Example
         
     | 
| 
      
 53 
     | 
    
         
            +
            -------
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
            One code snippet is worth 1000 words:
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 58 
     | 
    
         
            +
            # The anatomy of action class
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            # every action needs to inherit from Dynflow::Action
         
     | 
| 
      
 61 
     | 
    
         
            +
            class Action < Dynflow::Action
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
              # OPTIONAL: the input format for the execution phase of this action
         
     | 
| 
      
 64 
     | 
    
         
            +
              # (https://github.com/iNecas/apipie-params for more details.
         
     | 
| 
      
 65 
     | 
    
         
            +
              # Validations can be performed against this description (turned off
         
     | 
| 
      
 66 
     | 
    
         
            +
              # for now)
         
     | 
| 
      
 67 
     | 
    
         
            +
              input_format do
         
     | 
| 
      
 68 
     | 
    
         
            +
                param :id, Integer
         
     | 
| 
      
 69 
     | 
    
         
            +
                param :name, String
         
     | 
| 
      
 70 
     | 
    
         
            +
              end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
              # OPTIONAL: every action can produce an output in the execution
         
     | 
| 
      
 73 
     | 
    
         
            +
              # phase. This allows to describe the output.
         
     | 
| 
      
 74 
     | 
    
         
            +
              output_format do
         
     | 
| 
      
 75 
     | 
    
         
            +
                param :uuid, String
         
     | 
| 
      
 76 
     | 
    
         
            +
              end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
              # OPTIONAL: this specifies that this action should be performed when
         
     | 
| 
      
 79 
     | 
    
         
            +
              # AnotherAction is triggered.
         
     | 
| 
      
 80 
     | 
    
         
            +
              def self.subscribe
         
     | 
| 
      
 81 
     | 
    
         
            +
                AnotherAction
         
     | 
| 
      
 82 
     | 
    
         
            +
              end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
              # OPTIONAL: executed during the planning phase. It's possible to
         
     | 
| 
      
 85 
     | 
    
         
            +
              # specify explicitly the workflow here. By default it schedules just
         
     | 
| 
      
 86 
     | 
    
         
            +
              # this action.
         
     | 
| 
      
 87 
     | 
    
         
            +
              def plan(object_1, object_2)
         
     | 
| 
      
 88 
     | 
    
         
            +
                # +plan_action+ schedules the SubAction to be part of this
         
     | 
| 
      
 89 
     | 
    
         
            +
                # workflow
         
     | 
| 
      
 90 
     | 
    
         
            +
                # the +object_1+ is passed to the +SubAction#plan+ method.
         
     | 
| 
      
 91 
     | 
    
         
            +
                plan_action SubAction, object_1
         
     | 
| 
      
 92 
     | 
    
         
            +
                # we can specify, where in the workflow this action should be
         
     | 
| 
      
 93 
     | 
    
         
            +
                # placed, as well as prepare the input.
         
     | 
| 
      
 94 
     | 
    
         
            +
                plan_self {'id' => object_2.id, 'name' => object_2.name}
         
     | 
| 
      
 95 
     | 
    
         
            +
              end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
              # OPTIONAL: run the execution part of this action. Transform the
         
     | 
| 
      
 98 
     | 
    
         
            +
              # data from +input+ to +output+. When not specified, the action is
         
     | 
| 
      
 99 
     | 
    
         
            +
              # not used in the execution phase.
         
     | 
| 
      
 100 
     | 
    
         
            +
              def run
         
     | 
| 
      
 101 
     | 
    
         
            +
                output['uuid'] = "#{input['name']}-#{input['id']}"
         
     | 
| 
      
 102 
     | 
    
         
            +
              end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
              # OPTIONAL: finalize the action after the execution phase finishes.
         
     | 
| 
      
 105 
     | 
    
         
            +
              # in the +input+ and +output+ attributes are available the data from
         
     | 
| 
      
 106 
     | 
    
         
            +
              # execution phase. in the +outputs+ argument, all the execution
         
     | 
| 
      
 107 
     | 
    
         
            +
              # phase actions are available, each providing its input and output.
         
     | 
| 
      
 108 
     | 
    
         
            +
              def finalize(outputs)
         
     | 
| 
      
 109 
     | 
    
         
            +
                puts output['uuid']
         
     | 
| 
      
 110 
     | 
    
         
            +
              end
         
     | 
| 
      
 111 
     | 
    
         
            +
            end
         
     | 
| 
      
 112 
     | 
    
         
            +
            ```
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
            One can generate the execution plan for an action without actually
         
     | 
| 
      
 115 
     | 
    
         
            +
            running it:
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 118 
     | 
    
         
            +
            pp Publish.plan(short_article)
         
     | 
| 
      
 119 
     | 
    
         
            +
            # the expanded workflow is:
         
     | 
| 
      
 120 
     | 
    
         
            +
            # [
         
     | 
| 
      
 121 
     | 
    
         
            +
            #  [Publish, {"title"=>"Short", "body"=>"Short"}],
         
     | 
| 
      
 122 
     | 
    
         
            +
            #  [Review, {"title"=>"Short", "body"=>"Short"}],
         
     | 
| 
      
 123 
     | 
    
         
            +
            #  [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
         
     | 
| 
      
 124 
     | 
    
         
            +
            # ]
         
     | 
| 
      
 125 
     | 
    
         
            +
            ```
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
            Therefore it's suitable for the plan methods to not have any side
         
     | 
| 
      
 128 
     | 
    
         
            +
            effects (except of database writes that can be roll-backed)
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            In the finalization phase, `finalize` method is called on every action
         
     | 
| 
      
 131 
     | 
    
         
            +
            if defined. The order is the same as in the execution plan.
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
            Every action should be as atomic as possible, providing better
         
     | 
| 
      
 134 
     | 
    
         
            +
            granularity when manipulating the process. Since every action can be
         
     | 
| 
      
 135 
     | 
    
         
            +
            subscribed by another one, adding new behaviour to an existing
         
     | 
| 
      
 136 
     | 
    
         
            +
            workflow is really simple.
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
            The input and output format can be used for defining the interface
         
     | 
| 
      
 139 
     | 
    
         
            +
            that other developers can use when extending the workflows.
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
            See the examples directory for more complete examples.
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
            License
         
     | 
| 
      
 144 
     | 
    
         
            +
            -------
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
            MIT
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
            Author
         
     | 
| 
      
 149 
     | 
    
         
            +
            ------
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
            Ivan Nečas
         
     | 
    
        data/Rakefile
    ADDED
    
    
    
        data/dynflow.gemspec
    ADDED
    
    | 
         @@ -0,0 +1,23 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # -*- encoding: utf-8 -*-
         
     | 
| 
      
 2 
     | 
    
         
            +
            $:.push File.expand_path("../lib", __FILE__)
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "dynflow/version"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            Gem::Specification.new do |s|
         
     | 
| 
      
 6 
     | 
    
         
            +
              s.name        = "dynflow"
         
     | 
| 
      
 7 
     | 
    
         
            +
              s.version     = Dynflow::VERSION
         
     | 
| 
      
 8 
     | 
    
         
            +
              s.authors     = ["Ivan Necas"]
         
     | 
| 
      
 9 
     | 
    
         
            +
              s.email       = ["inecas@redhat.com"]
         
     | 
| 
      
 10 
     | 
    
         
            +
              s.homepage    = "http://github.com/iNecas/eventum"
         
     | 
| 
      
 11 
     | 
    
         
            +
              s.summary     = "DYNamic workFLOW engine"
         
     | 
| 
      
 12 
     | 
    
         
            +
              s.description = "Generate and executed workflows dynamically based "+
         
     | 
| 
      
 13 
     | 
    
         
            +
                              "on input data and leave it open for others to jump into it as well"
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              s.files         = `git ls-files`.split("\n")
         
     | 
| 
      
 16 
     | 
    
         
            +
              s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
         
     | 
| 
      
 17 
     | 
    
         
            +
              s.require_paths = ["lib"]
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              s.add_dependency "activesupport"
         
     | 
| 
      
 20 
     | 
    
         
            +
              s.add_dependency "multi_json"
         
     | 
| 
      
 21 
     | 
    
         
            +
              s.add_dependency "apipie-params"
         
     | 
| 
      
 22 
     | 
    
         
            +
              s.add_development_dependency "minitest"
         
     | 
| 
      
 23 
     | 
    
         
            +
            end
         
     | 
    
        data/examples/events.rb
    ADDED
    
    | 
         @@ -0,0 +1,71 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Shows how Dynflow can be used for events architecture: actions are
         
     | 
| 
      
 2 
     | 
    
         
            +
            # subscribed to an event. When the event is triggered all the
         
     | 
| 
      
 3 
     | 
    
         
            +
            # subscribed actions are preformed.
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            $:.unshift(File.expand_path('../../lib', __FILE__))
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            require 'dynflow'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'pp'
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            # this is an event that can be triggered.
         
     | 
| 
      
 11 
     | 
    
         
            +
            # it has an input format so that the interface is given
         
     | 
| 
      
 12 
     | 
    
         
            +
            # TODO: the validations are turned off right now
         
     | 
| 
      
 13 
     | 
    
         
            +
            class Click < Dynflow::Action
         
     | 
| 
      
 14 
     | 
    
         
            +
              input_format do
         
     | 
| 
      
 15 
     | 
    
         
            +
                param :x, Integer
         
     | 
| 
      
 16 
     | 
    
         
            +
                param :y, Integer
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
            end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            # SayHello subscibes to the event: it's run when the event is triggered
         
     | 
| 
      
 21 
     | 
    
         
            +
            class SayHello < Dynflow::Action
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              def self.subscribe
         
     | 
| 
      
 24 
     | 
    
         
            +
                Click
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              def run
         
     | 
| 
      
 28 
     | 
    
         
            +
                puts "Hello World"
         
     | 
| 
      
 29 
     | 
    
         
            +
              end
         
     | 
| 
      
 30 
     | 
    
         
            +
            end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            # we can subscribe more actions to an event
         
     | 
| 
      
 33 
     | 
    
         
            +
            class SayPosition < Dynflow::Action
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              def self.subscribe
         
     | 
| 
      
 36 
     | 
    
         
            +
                Click
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              def run
         
     | 
| 
      
 40 
     | 
    
         
            +
                puts "your position is [#{input['x']} - #{input['y']}]"
         
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            # we can even subscribe to an action that is subscribed to an event
         
     | 
| 
      
 46 
     | 
    
         
            +
            class SayGoodbye < Dynflow::Action
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
              def self.subscribe
         
     | 
| 
      
 49 
     | 
    
         
            +
                SayPosition
         
     | 
| 
      
 50 
     | 
    
         
            +
              end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
              def run
         
     | 
| 
      
 53 
     | 
    
         
            +
                puts "Good Bye"
         
     | 
| 
      
 54 
     | 
    
         
            +
              end
         
     | 
| 
      
 55 
     | 
    
         
            +
            end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            Click.trigger('x' => 5, 'y' => 4)
         
     | 
| 
      
 58 
     | 
    
         
            +
            # gives us:
         
     | 
| 
      
 59 
     | 
    
         
            +
            # Hello World
         
     | 
| 
      
 60 
     | 
    
         
            +
            # your position is [5 - 4]
         
     | 
| 
      
 61 
     | 
    
         
            +
            # Good Bye
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
            pp Click.plan('x' => 5, 'y' => 4)
         
     | 
| 
      
 64 
     | 
    
         
            +
            # returns the execution plan for the event (nothing is triggered):
         
     | 
| 
      
 65 
     | 
    
         
            +
            # [
         
     | 
| 
      
 66 
     | 
    
         
            +
            # since the event is action as well, it could have a run method
         
     | 
| 
      
 67 
     | 
    
         
            +
            #  [Click,        {"x"=>5, "y"=>4}],
         
     | 
| 
      
 68 
     | 
    
         
            +
            #  [SayHello,     {"x"=>5, "y"=>4}],
         
     | 
| 
      
 69 
     | 
    
         
            +
            #  [SayPosition,  {"x"=>5, "y"=>4}],
         
     | 
| 
      
 70 
     | 
    
         
            +
            #  [SayGoodbye,   {"x"=>5, "y"=>4}]
         
     | 
| 
      
 71 
     | 
    
         
            +
            # ]
         
     | 
| 
         @@ -0,0 +1,140 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Shows how Dynflow can be used for dynamic workflow definition
         
     | 
| 
      
 2 
     | 
    
         
            +
            # and execution.
         
     | 
| 
      
 3 
     | 
    
         
            +
            # In a planning phase of an action, a sub-action can be planned as
         
     | 
| 
      
 4 
     | 
    
         
            +
            # well.
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            $:.unshift(File.expand_path('../../lib', __FILE__))
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            require 'dynflow'
         
     | 
| 
      
 10 
     | 
    
         
            +
            require 'pp'
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            class Article < Struct.new(:title, :body, :color); end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            class Publish < Dynflow::Action
         
     | 
| 
      
 15 
     | 
    
         
            +
              input_format do
         
     | 
| 
      
 16 
     | 
    
         
            +
                param :title, Integer
         
     | 
| 
      
 17 
     | 
    
         
            +
                param :body, Integer
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
              # plan can take arbitrary arguments. The args are passed from the
         
     | 
| 
      
 21 
     | 
    
         
            +
              # trigger method.
         
     | 
| 
      
 22 
     | 
    
         
            +
              def plan(article)
         
     | 
| 
      
 23 
     | 
    
         
            +
                # we can explicitly plan a subaction
         
     | 
| 
      
 24 
     | 
    
         
            +
                plan_self 'title' => article.title, 'body' => article.body
         
     | 
| 
      
 25 
     | 
    
         
            +
                plan_action Review, article
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              def run
         
     | 
| 
      
 29 
     | 
    
         
            +
                puts 'Starting'
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
              # after all actions are run, there is a finishing phase. All the
         
     | 
| 
      
 33 
     | 
    
         
            +
              # actions with +finished+ action defined are called, passing all the
         
     | 
| 
      
 34 
     | 
    
         
            +
              # performed actions (with inputs and outputs)
         
     | 
| 
      
 35 
     | 
    
         
            +
              def finalize(outputs)
         
     | 
| 
      
 36 
     | 
    
         
            +
                printer_action = outputs.find { |o| o.is_a? Print }
         
     | 
| 
      
 37 
     | 
    
         
            +
                puts "Printer says '#{printer_action.output['message']}'"
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            class Review < Dynflow::Action
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              # the actions can provide an output for the finalizing phase
         
     | 
| 
      
 44 
     | 
    
         
            +
              output_format do
         
     | 
| 
      
 45 
     | 
    
         
            +
                param :rating, Integer
         
     | 
| 
      
 46 
     | 
    
         
            +
              end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
              # the plan method takes the same arguments as the parent action
         
     | 
| 
      
 49 
     | 
    
         
            +
              def plan(article)
         
     | 
| 
      
 50 
     | 
    
         
            +
                # in the input attribute the input for the parent action is
         
     | 
| 
      
 51 
     | 
    
         
            +
                # available
         
     | 
| 
      
 52 
     | 
    
         
            +
                plan_self input
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
              # if no plan method given, the input is the same as the action that
         
     | 
| 
      
 56 
     | 
    
         
            +
              # triggered it
         
     | 
| 
      
 57 
     | 
    
         
            +
              def run
         
     | 
| 
      
 58 
     | 
    
         
            +
                puts "Reviewing #{input['title']}"
         
     | 
| 
      
 59 
     | 
    
         
            +
                raise "Too Short" if input['body'].size < 6
         
     | 
| 
      
 60 
     | 
    
         
            +
                output['rating'] = input['body'].size
         
     | 
| 
      
 61 
     | 
    
         
            +
              end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
              def finalize(outputs)
         
     | 
| 
      
 64 
     | 
    
         
            +
                # +input+ and +output+ attributes are available in the finalizing
         
     | 
| 
      
 65 
     | 
    
         
            +
                # phase as well.
         
     | 
| 
      
 66 
     | 
    
         
            +
                puts "The rating was #{output['rating']}"
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
            end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            class Print < Dynflow::Action
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
              input_format do
         
     | 
| 
      
 74 
     | 
    
         
            +
                param :title, Integer
         
     | 
| 
      
 75 
     | 
    
         
            +
                param :body, Integer
         
     | 
| 
      
 76 
     | 
    
         
            +
                param :color, :boolean
         
     | 
| 
      
 77 
     | 
    
         
            +
              end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
              output_format do
         
     | 
| 
      
 80 
     | 
    
         
            +
                param :message, String
         
     | 
| 
      
 81 
     | 
    
         
            +
              end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
              # if needed, we can subscribe to an action instead of explicitly
         
     | 
| 
      
 84 
     | 
    
         
            +
              # specifying it in the plan method. Suitable for plugin architecture.
         
     | 
| 
      
 85 
     | 
    
         
            +
              def self.subscribe
         
     | 
| 
      
 86 
     | 
    
         
            +
                Review # sucessful review means we can print
         
     | 
| 
      
 87 
     | 
    
         
            +
              end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
              def plan(article)
         
     | 
| 
      
 90 
     | 
    
         
            +
                plan_self input.merge('color' => article.color)
         
     | 
| 
      
 91 
     | 
    
         
            +
              end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
              def run
         
     | 
| 
      
 94 
     | 
    
         
            +
                if input['color']
         
     | 
| 
      
 95 
     | 
    
         
            +
                  puts "Printing in color"
         
     | 
| 
      
 96 
     | 
    
         
            +
                else
         
     | 
| 
      
 97 
     | 
    
         
            +
                  puts "Printing blank&white"
         
     | 
| 
      
 98 
     | 
    
         
            +
                end
         
     | 
| 
      
 99 
     | 
    
         
            +
                output['message'] = "Here you are"
         
     | 
| 
      
 100 
     | 
    
         
            +
              end
         
     | 
| 
      
 101 
     | 
    
         
            +
            end
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
            short_article = Article.new('Short', 'Short', false)
         
     | 
| 
      
 104 
     | 
    
         
            +
            long_article = Article.new('Long', 'This is long', false)
         
     | 
| 
      
 105 
     | 
    
         
            +
            colorful_article = Article.new('Long Color', 'This is long in color', true)
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
            pp Publish.plan(short_article)
         
     | 
| 
      
 108 
     | 
    
         
            +
            # the expanded workflow is:
         
     | 
| 
      
 109 
     | 
    
         
            +
            # [
         
     | 
| 
      
 110 
     | 
    
         
            +
            #  [Publish, {"title"=>"Short", "body"=>"Short"}],
         
     | 
| 
      
 111 
     | 
    
         
            +
            #  [Review, {"title"=>"Short", "body"=>"Short"}],
         
     | 
| 
      
 112 
     | 
    
         
            +
            #  [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
         
     | 
| 
      
 113 
     | 
    
         
            +
            # ]
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
            begin
         
     | 
| 
      
 116 
     | 
    
         
            +
              Publish.trigger(short_article)
         
     | 
| 
      
 117 
     | 
    
         
            +
            rescue => e
         
     | 
| 
      
 118 
     | 
    
         
            +
              puts e.message
         
     | 
| 
      
 119 
     | 
    
         
            +
            end
         
     | 
| 
      
 120 
     | 
    
         
            +
            # Produces:
         
     | 
| 
      
 121 
     | 
    
         
            +
            # Starting
         
     | 
| 
      
 122 
     | 
    
         
            +
            # Reviewing Short
         
     | 
| 
      
 123 
     | 
    
         
            +
            # Too Short
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
            Publish.trigger(long_article)
         
     | 
| 
      
 126 
     | 
    
         
            +
            # Produces:
         
     | 
| 
      
 127 
     | 
    
         
            +
            # Starting
         
     | 
| 
      
 128 
     | 
    
         
            +
            # Reviewing Long
         
     | 
| 
      
 129 
     | 
    
         
            +
            # Printing blank&white
         
     | 
| 
      
 130 
     | 
    
         
            +
            # Printer says 'Here you are'
         
     | 
| 
      
 131 
     | 
    
         
            +
            # The rating was 12
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
            Publish.trigger(colorful_article)
         
     | 
| 
      
 135 
     | 
    
         
            +
            # Produces:
         
     | 
| 
      
 136 
     | 
    
         
            +
            # Starting
         
     | 
| 
      
 137 
     | 
    
         
            +
            # Reviewing Long Color
         
     | 
| 
      
 138 
     | 
    
         
            +
            # Printing in color
         
     | 
| 
      
 139 
     | 
    
         
            +
            # Printer says 'Here you are'
         
     | 
| 
      
 140 
     | 
    
         
            +
            # The rating was 21
         
     | 
    
        data/lib/dynflow.rb
    ADDED
    
    
| 
         @@ -0,0 +1,121 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Action < Message
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                # only for the planning phase: flag indicating that the action
         
     | 
| 
      
 5 
     | 
    
         
            +
                # was triggered from subscription. If so, the implicit plan
         
     | 
| 
      
 6 
     | 
    
         
            +
                # method uses the input of the parent action. Otherwise, the
         
     | 
| 
      
 7 
     | 
    
         
            +
                # argument the plan_action is used as default.
         
     | 
| 
      
 8 
     | 
    
         
            +
                attr_accessor :from_subscription
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def self.inherited(child)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  self.actions << child
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def self.actions
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @actions ||= []
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def self.subscribe
         
     | 
| 
      
 19 
     | 
    
         
            +
                  nil
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def self.require
         
     | 
| 
      
 23 
     | 
    
         
            +
                  nil
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def initialize(input, output = {})
         
     | 
| 
      
 27 
     | 
    
         
            +
                  # for preparation phase
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @execution_plan = []
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  output ||= {}
         
     | 
| 
      
 31 
     | 
    
         
            +
                  super('input' => input, 'output' => output)
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def input
         
     | 
| 
      
 35 
     | 
    
         
            +
                  @data['input']
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                def input=(input)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  @data['input'] = input
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                def output
         
     | 
| 
      
 43 
     | 
    
         
            +
                  @data['output']
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                # the block contains the expression in Apipie::Params::DSL
         
     | 
| 
      
 47 
     | 
    
         
            +
                # describing the format of message
         
     | 
| 
      
 48 
     | 
    
         
            +
                def self.input_format(&block)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  if block
         
     | 
| 
      
 50 
     | 
    
         
            +
                    @input_format_block = block
         
     | 
| 
      
 51 
     | 
    
         
            +
                  elsif @input_format_block
         
     | 
| 
      
 52 
     | 
    
         
            +
                    @input_format ||= Apipie::Params::Description.define(&@input_format_block)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  else
         
     | 
| 
      
 54 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                # the block contains the expression in Apipie::Params::DSL
         
     | 
| 
      
 59 
     | 
    
         
            +
                # describing the format of message
         
     | 
| 
      
 60 
     | 
    
         
            +
                def self.output_format(&block)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  if block
         
     | 
| 
      
 62 
     | 
    
         
            +
                    @output_format_block = block
         
     | 
| 
      
 63 
     | 
    
         
            +
                  elsif @output_format_block
         
     | 
| 
      
 64 
     | 
    
         
            +
                    @output_format ||= Apipie::Params::Description.define(&@output_format_block)
         
     | 
| 
      
 65 
     | 
    
         
            +
                  else
         
     | 
| 
      
 66 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 67 
     | 
    
         
            +
                  end
         
     | 
| 
      
 68 
     | 
    
         
            +
                end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                def self.trigger(*args)
         
     | 
| 
      
 71 
     | 
    
         
            +
                  Dynflow::Bus.trigger(self.plan(*args))
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                def self.plan(*args)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  action = self.new({})
         
     | 
| 
      
 76 
     | 
    
         
            +
                  yield action if block_given?
         
     | 
| 
      
 77 
     | 
    
         
            +
                  action.plan(*args)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  action.add_subscriptions(*args)
         
     | 
| 
      
 79 
     | 
    
         
            +
                  action.execution_plan
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                # for subscribed actions: by default take the input of the
         
     | 
| 
      
 83 
     | 
    
         
            +
                # subscribed action
         
     | 
| 
      
 84 
     | 
    
         
            +
                def plan(*args)
         
     | 
| 
      
 85 
     | 
    
         
            +
                  if from_subscription
         
     | 
| 
      
 86 
     | 
    
         
            +
                    # if the action is triggered by subscription, by default use the
         
     | 
| 
      
 87 
     | 
    
         
            +
                    # input of parent action
         
     | 
| 
      
 88 
     | 
    
         
            +
                    plan_self(self.input)
         
     | 
| 
      
 89 
     | 
    
         
            +
                  else
         
     | 
| 
      
 90 
     | 
    
         
            +
                    # in this case, the action was triggered by plan_action. Use
         
     | 
| 
      
 91 
     | 
    
         
            +
                    # the argument specified there.
         
     | 
| 
      
 92 
     | 
    
         
            +
                    plan_self(args.first)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                def plan_self(input)
         
     | 
| 
      
 97 
     | 
    
         
            +
                  self.input = input
         
     | 
| 
      
 98 
     | 
    
         
            +
                  @execution_plan << [self.class, input]
         
     | 
| 
      
 99 
     | 
    
         
            +
                end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                def plan_action(action_class, *args)
         
     | 
| 
      
 102 
     | 
    
         
            +
                  sub_action_plan = action_class.plan(*args) do |action|
         
     | 
| 
      
 103 
     | 
    
         
            +
                    action.input = self.input
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
                  @execution_plan.concat(sub_action_plan)
         
     | 
| 
      
 106 
     | 
    
         
            +
                end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                def add_subscriptions(*plan_args)
         
     | 
| 
      
 109 
     | 
    
         
            +
                  @execution_plan.concat(Dispatcher.execution_plan_for(self, *plan_args))
         
     | 
| 
      
 110 
     | 
    
         
            +
                end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                def execution_plan
         
     | 
| 
      
 113 
     | 
    
         
            +
                  @execution_plan
         
     | 
| 
      
 114 
     | 
    
         
            +
                end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                def validate!
         
     | 
| 
      
 117 
     | 
    
         
            +
                  self.clss.output_format.validate!(@data['output'])
         
     | 
| 
      
 118 
     | 
    
         
            +
                end
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
              end
         
     | 
| 
      
 121 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/dynflow/bus.rb
    ADDED
    
    | 
         @@ -0,0 +1,56 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'active_support/inflector'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'forwardable'
         
     | 
| 
      
 3 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Bus
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 7 
     | 
    
         
            +
                  extend Forwardable
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  def_delegators :impl, :wait_for, :process, :trigger, :finalize
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def impl
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @impl ||= Bus::MemoryBus.new
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
                  attr_writer :impl
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def finalize(outputs)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  outputs.each do |action|
         
     | 
| 
      
 19 
     | 
    
         
            +
                    if action.respond_to?(:finalize)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      action.finalize(outputs)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    end
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                def process(action_class, input, output = nil)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  # TODO: here goes the message validation
         
     | 
| 
      
 27 
     | 
    
         
            +
                  action = action_class.new(input, output)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  action.run if action.respond_to?(:run)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  return action
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def wait_for(*args)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  raise NotImplementedError, 'Abstract method'
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def logger
         
     | 
| 
      
 37 
     | 
    
         
            +
                  @logger ||= Dynflow::Logger.new(self.class)
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                class MemoryBus < Bus
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 43 
     | 
    
         
            +
                    super
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  def trigger(execution_plan)
         
     | 
| 
      
 47 
     | 
    
         
            +
                    outputs = []
         
     | 
| 
      
 48 
     | 
    
         
            +
                    execution_plan.each do |(action_class, input)|
         
     | 
| 
      
 49 
     | 
    
         
            +
                      outputs << self.process(action_class, input)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end
         
     | 
| 
      
 51 
     | 
    
         
            +
                    self.finalize(outputs)
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                end
         
     | 
| 
      
 55 
     | 
    
         
            +
              end
         
     | 
| 
      
 56 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,36 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Dispatcher
         
     | 
| 
      
 3 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 4 
     | 
    
         
            +
                  def finalizers
         
     | 
| 
      
 5 
     | 
    
         
            +
                    @finalizers ||= Hash.new { |h, k| h[k] = [] }
         
     | 
| 
      
 6 
     | 
    
         
            +
                  end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def subscribed_actions(action)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    Action.actions.find_all do |sub_action|
         
     | 
| 
      
 10 
     | 
    
         
            +
                      case sub_action.subscribe
         
     | 
| 
      
 11 
     | 
    
         
            +
                      when Hash
         
     | 
| 
      
 12 
     | 
    
         
            +
                        sub_action.subscribe.keys.include?(action.class)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      when Array
         
     | 
| 
      
 14 
     | 
    
         
            +
                        sub_action.subscribe.include?(action.class)
         
     | 
| 
      
 15 
     | 
    
         
            +
                      else
         
     | 
| 
      
 16 
     | 
    
         
            +
                        sub_action.subscribe == action.class
         
     | 
| 
      
 17 
     | 
    
         
            +
                      end
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  def execution_plan_for(action, *plan_args)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    ordered_actions = subscribed_actions(action).sort_by(&:name)
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                    execution_plan = []
         
     | 
| 
      
 25 
     | 
    
         
            +
                    ordered_actions.each do |action_class|
         
     | 
| 
      
 26 
     | 
    
         
            +
                      sub_action_plan = action_class.plan(*plan_args) do |sub_action|
         
     | 
| 
      
 27 
     | 
    
         
            +
                        sub_action.input = action.input
         
     | 
| 
      
 28 
     | 
    
         
            +
                        sub_action.from_subscription = true
         
     | 
| 
      
 29 
     | 
    
         
            +
                      end
         
     | 
| 
      
 30 
     | 
    
         
            +
                      execution_plan.concat(sub_action_plan)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
                    return execution_plan
         
     | 
| 
      
 33 
     | 
    
         
            +
                  end
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
              end
         
     | 
| 
      
 36 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,34 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'forwardable'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'logger'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Logger
         
     | 
| 
      
 6 
     | 
    
         
            +
                extend Forwardable
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def_delegators :@impl, :debug, :info, :warn; :error
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                class DummyLogger < ::Logger
         
     | 
| 
      
 11 
     | 
    
         
            +
                  def initialize(identifier)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    super(nil)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def initialize(identifier, impl = nil)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @impl = self.class.logger_class.new(identifier)
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  def logger_class
         
     | 
| 
      
 24 
     | 
    
         
            +
                    unless @logger_class
         
     | 
| 
      
 25 
     | 
    
         
            +
                      @logger_class ||= DummyLogger
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
                    return @logger_class
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  attr_writer :logger_class
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'forwardable'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'active_support/core_ext/hash/indifferent_access'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'active_support/core_ext/string/inflections'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'apipie-params'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 7 
     | 
    
         
            +
              class Message
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                def ==(other)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  self.encode == other.encode
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                extend Forwardable
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def_delegators :@data, '[]', '[]='
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                attr_reader :data
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def initialize(data = {})
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @data = data.with_indifferent_access
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                def self.decode(data)
         
     | 
| 
      
 25 
     | 
    
         
            +
                  ret = data['message_type'].constantize.allocate
         
     | 
| 
      
 26 
     | 
    
         
            +
                  ret.instance_variable_set("@data", data['data'])
         
     | 
| 
      
 27 
     | 
    
         
            +
                  return ret
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                def encode
         
     | 
| 
      
 31 
     | 
    
         
            +
                  {
         
     | 
| 
      
 32 
     | 
    
         
            +
                    'message_type' => self.class.name,
         
     | 
| 
      
 33 
     | 
    
         
            +
                    'data' => @data
         
     | 
| 
      
 34 
     | 
    
         
            +
                  }
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,14 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 2 
     | 
    
         
            +
              class OrchRequest < Message
         
     | 
| 
      
 3 
     | 
    
         
            +
                def self.response_class
         
     | 
| 
      
 4 
     | 
    
         
            +
                  unless self.name =~ /::Request\Z/
         
     | 
| 
      
 5 
     | 
    
         
            +
                    raise "Unexpected class name, #{self.name} expected to end with ::Request"
         
     | 
| 
      
 6 
     | 
    
         
            +
                  end
         
     | 
| 
      
 7 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 8 
     | 
    
         
            +
                    self.name.sub(/::Request\Z/, '::Response').constantize
         
     | 
| 
      
 9 
     | 
    
         
            +
                  rescue NameError => e
         
     | 
| 
      
 10 
     | 
    
         
            +
                    OrchResponse
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
            end
         
     | 
    
        data/test/action_test.rb
    ADDED
    
    | 
         @@ -0,0 +1,24 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'test_helper'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 4 
     | 
    
         
            +
              class CloneRepo < Action
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                output_format do
         
     | 
| 
      
 7 
     | 
    
         
            +
                  param :id, String
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def run
         
     | 
| 
      
 11 
     | 
    
         
            +
                  output['id'] = input['name']
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
              end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
              class CloneRepoTest < ParticipantTestCase
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def test_action
         
     | 
| 
      
 19 
     | 
    
         
            +
                  action = run_action(CloneRepo, {:name => "zoo"})
         
     | 
| 
      
 20 
     | 
    
         
            +
                  assert_equal(action.output['id'], "zoo")
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
    
        data/test/bus_test.rb
    ADDED
    
    | 
         @@ -0,0 +1,55 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'test_helper'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'set'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 5 
     | 
    
         
            +
              class BusTest < BusTestCase
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Promotion < Action
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def plan(repo_names, package_names)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    repo_names.each do |repo_name|
         
     | 
| 
      
 10 
     | 
    
         
            +
                      plan_action(CloneRepo, {'name' => repo_name})
         
     | 
| 
      
 11 
     | 
    
         
            +
                    end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                    package_names.each do |package_name|
         
     | 
| 
      
 14 
     | 
    
         
            +
                      plan_action(ClonePackage, {'name' => package_name})
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                class CloneRepo < Action
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  input_format do
         
     | 
| 
      
 23 
     | 
    
         
            +
                    param :name, String
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  output_format do
         
     | 
| 
      
 27 
     | 
    
         
            +
                    param :id, String
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  def run
         
     | 
| 
      
 31 
     | 
    
         
            +
                    output['id'] = input['name']
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def execution_plan
         
     | 
| 
      
 37 
     | 
    
         
            +
                  [
         
     | 
| 
      
 38 
     | 
    
         
            +
                   [CloneRepo, {'name' => 'zoo'}],
         
     | 
| 
      
 39 
     | 
    
         
            +
                   [CloneRepo, {'name' => 'foo'}],
         
     | 
| 
      
 40 
     | 
    
         
            +
                  ]
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def test_optimistic_case
         
     | 
| 
      
 44 
     | 
    
         
            +
                  expect_input(CloneRepo, {'name' => 'zoo'}, {'id' => '123'})
         
     | 
| 
      
 45 
     | 
    
         
            +
                  expect_input(CloneRepo, {'name' => 'foo'}, {'id' => '456'})
         
     | 
| 
      
 46 
     | 
    
         
            +
                  first_action, second_action = assert_scenario
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  assert_equal({'name' => 'zoo'}, first_action.input)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  assert_equal({'id' => '123'},   first_action.output)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  assert_equal({'name' => 'foo'}, second_action.input)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  assert_equal({'id' => '456'},   second_action.output)
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
              end
         
     | 
| 
      
 55 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,108 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'test_helper'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Dynflow
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              describe Dispatcher do
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Promotion < Action
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def plan(repo_names, package_names)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    repo_names.each do |repo_name|
         
     | 
| 
      
 10 
     | 
    
         
            +
                      plan_action(CloneRepo, {'name' => repo_name})
         
     | 
| 
      
 11 
     | 
    
         
            +
                    end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                    package_names.each do |package_name|
         
     | 
| 
      
 14 
     | 
    
         
            +
                      plan_action(ClonePackage, {'name' => package_name})
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    plan_self('actions' => repo_names.size + package_names.size)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  input_format do
         
     | 
| 
      
 21 
     | 
    
         
            +
                    param :actions, Integer
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                class PromotionObserver < Action
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  def self.subscribe
         
     | 
| 
      
 29 
     | 
    
         
            +
                    Promotion
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                class CloneRepo < Action
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  input_format do
         
     | 
| 
      
 37 
     | 
    
         
            +
                    param :name, String
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  output_format do
         
     | 
| 
      
 41 
     | 
    
         
            +
                    param :id, String
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                class ClonePackage < Action
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  input_format do
         
     | 
| 
      
 49 
     | 
    
         
            +
                    param :name, String
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  output_format do
         
     | 
| 
      
 53 
     | 
    
         
            +
                    param :id, String
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                class UpdateIndex < Action
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                  def self.subscribe
         
     | 
| 
      
 61 
     | 
    
         
            +
                    ClonePackage
         
     | 
| 
      
 62 
     | 
    
         
            +
                  end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  def plan(input)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    plan_action(YetAnotherAction, {'hello' => 'world'})
         
     | 
| 
      
 66 
     | 
    
         
            +
                    super
         
     | 
| 
      
 67 
     | 
    
         
            +
                  end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                  output_format do
         
     | 
| 
      
 70 
     | 
    
         
            +
                    param :indexed_name, String
         
     | 
| 
      
 71 
     | 
    
         
            +
                  end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                class YetAnotherAction < Action
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                  input_format do
         
     | 
| 
      
 78 
     | 
    
         
            +
                    param :name, String
         
     | 
| 
      
 79 
     | 
    
         
            +
                    param :hello, String
         
     | 
| 
      
 80 
     | 
    
         
            +
                  end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                  output_format do
         
     | 
| 
      
 83 
     | 
    
         
            +
                    param :hello, String
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  def plan(arg)
         
     | 
| 
      
 87 
     | 
    
         
            +
                    plan_self(input.merge(arg))
         
     | 
| 
      
 88 
     | 
    
         
            +
                  end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                end
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                it "builds the execution plan" do
         
     | 
| 
      
 93 
     | 
    
         
            +
                  execution_plan = Promotion.plan(['zoo', 'foo'], ['elephant'])
         
     | 
| 
      
 94 
     | 
    
         
            +
                  expected_plan =
         
     | 
| 
      
 95 
     | 
    
         
            +
                    [
         
     | 
| 
      
 96 
     | 
    
         
            +
                     [CloneRepo, {'name' => 'zoo'}],
         
     | 
| 
      
 97 
     | 
    
         
            +
                     [CloneRepo, {'name' => 'foo'}],
         
     | 
| 
      
 98 
     | 
    
         
            +
                     [ClonePackage, {'name' => 'elephant'}],
         
     | 
| 
      
 99 
     | 
    
         
            +
                     [YetAnotherAction, {'name' => 'elephant', 'hello' => 'world'}],
         
     | 
| 
      
 100 
     | 
    
         
            +
                     [UpdateIndex, {'name' => 'elephant'}],
         
     | 
| 
      
 101 
     | 
    
         
            +
                     [Promotion, {'actions' => 3 }],
         
     | 
| 
      
 102 
     | 
    
         
            +
                     [PromotionObserver, {'actions' => 3 }]
         
     | 
| 
      
 103 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 104 
     | 
    
         
            +
                  execution_plan.must_equal expected_plan
         
     | 
| 
      
 105 
     | 
    
         
            +
                end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
              end
         
     | 
| 
      
 108 
     | 
    
         
            +
            end
         
     | 
    
        data/test/test_helper.rb
    ADDED
    
    | 
         @@ -0,0 +1,83 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'test/unit'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'minitest/spec'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'dynflow'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            BUS_IMPL = Dynflow::Bus::MemoryBus
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            class TestBus < BUS_IMPL
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              def initialize(expected_scenario)
         
     | 
| 
      
 10 
     | 
    
         
            +
                super()
         
     | 
| 
      
 11 
     | 
    
         
            +
                @expected_scenario = expected_scenario
         
     | 
| 
      
 12 
     | 
    
         
            +
              end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
              def process(action_class, input, output = nil, stub = true)
         
     | 
| 
      
 15 
     | 
    
         
            +
                expected = @expected_scenario.shift
         
     | 
| 
      
 16 
     | 
    
         
            +
                if action_class == TestScenarioFinalizer || !stub || output
         
     | 
| 
      
 17 
     | 
    
         
            +
                  return super(action_class, input, output)
         
     | 
| 
      
 18 
     | 
    
         
            +
                elsif action_class.name == expected[:action_class].name && input == expected[:input]
         
     | 
| 
      
 19 
     | 
    
         
            +
                  return action_class.new(expected[:input], expected[:output])
         
     | 
| 
      
 20 
     | 
    
         
            +
                else
         
     | 
| 
      
 21 
     | 
    
         
            +
                  raise "Unexpected input. Expected #{expected[:action_class]} #{expected[:input].inspect}, got #{action_class} #{input.inspect}"
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            class TestScenarioFinalizer < Dynflow::Action
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
              class << self
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def recorded_outputs
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @recorded_outputs
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def init_recorded_outputs
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @recorded_outputs = []
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                def save_recorded_outputs(recorded_outputs)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  @recorded_outputs = recorded_outputs
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
              def finalize(outputs)
         
     | 
| 
      
 46 
     | 
    
         
            +
                self.class.save_recorded_outputs(outputs)
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            class BusTestCase < Test::Unit::TestCase
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
              def setup
         
     | 
| 
      
 54 
     | 
    
         
            +
                @expected_scenario = []
         
     | 
| 
      
 55 
     | 
    
         
            +
              end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
              def expect_input(action_class, input, output)
         
     | 
| 
      
 58 
     | 
    
         
            +
                @expected_scenario << {
         
     | 
| 
      
 59 
     | 
    
         
            +
                  :action_class => action_class,
         
     | 
| 
      
 60 
     | 
    
         
            +
                  :input => input,
         
     | 
| 
      
 61 
     | 
    
         
            +
                  :output => output
         
     | 
| 
      
 62 
     | 
    
         
            +
                }
         
     | 
| 
      
 63 
     | 
    
         
            +
              end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
              def assert_scenario
         
     | 
| 
      
 66 
     | 
    
         
            +
                Dynflow::Bus.impl = TestBus.new(@expected_scenario)
         
     | 
| 
      
 67 
     | 
    
         
            +
                event_outputs = nil
         
     | 
| 
      
 68 
     | 
    
         
            +
                TestScenarioFinalizer.init_recorded_outputs
         
     | 
| 
      
 69 
     | 
    
         
            +
                execution_plan = self.execution_plan
         
     | 
| 
      
 70 
     | 
    
         
            +
                execution_plan << [TestScenarioFinalizer, {}]
         
     | 
| 
      
 71 
     | 
    
         
            +
                Dynflow::Bus.trigger(execution_plan)
         
     | 
| 
      
 72 
     | 
    
         
            +
                return TestScenarioFinalizer.recorded_outputs
         
     | 
| 
      
 73 
     | 
    
         
            +
              end
         
     | 
| 
      
 74 
     | 
    
         
            +
            end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
            class ParticipantTestCase < Test::Unit::TestCase
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
              def run_action(action_class, input)
         
     | 
| 
      
 79 
     | 
    
         
            +
                Dynflow::Bus.impl = Dynflow::Bus.new
         
     | 
| 
      
 80 
     | 
    
         
            +
                output = Dynflow::Bus.process(action_class, input)
         
     | 
| 
      
 81 
     | 
    
         
            +
                return output
         
     | 
| 
      
 82 
     | 
    
         
            +
              end
         
     | 
| 
      
 83 
     | 
    
         
            +
            end
         
     | 
    
        metadata
    ADDED
    
    | 
         @@ -0,0 +1,135 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            --- !ruby/object:Gem::Specification
         
     | 
| 
      
 2 
     | 
    
         
            +
            name: dynflow
         
     | 
| 
      
 3 
     | 
    
         
            +
            version: !ruby/object:Gem::Version
         
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.0.1
         
     | 
| 
      
 5 
     | 
    
         
            +
              prerelease: 
         
     | 
| 
      
 6 
     | 
    
         
            +
            platform: ruby
         
     | 
| 
      
 7 
     | 
    
         
            +
            authors:
         
     | 
| 
      
 8 
     | 
    
         
            +
            - Ivan Necas
         
     | 
| 
      
 9 
     | 
    
         
            +
            autorequire: 
         
     | 
| 
      
 10 
     | 
    
         
            +
            bindir: bin
         
     | 
| 
      
 11 
     | 
    
         
            +
            cert_chain: []
         
     | 
| 
      
 12 
     | 
    
         
            +
            date: 2013-04-24 00:00:00.000000000 Z
         
     | 
| 
      
 13 
     | 
    
         
            +
            dependencies:
         
     | 
| 
      
 14 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 15 
     | 
    
         
            +
              name: activesupport
         
     | 
| 
      
 16 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 17 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 18 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 19 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 20 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 21 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 22 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 23 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 24 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 25 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 26 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 27 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 28 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 29 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 30 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 31 
     | 
    
         
            +
              name: multi_json
         
     | 
| 
      
 32 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 33 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 34 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 35 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 36 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 37 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 38 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 39 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 40 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 41 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 42 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 43 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 44 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 45 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 46 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 47 
     | 
    
         
            +
              name: apipie-params
         
     | 
| 
      
 48 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 49 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 50 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 51 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 52 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 53 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 54 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 55 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 56 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 57 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 58 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 59 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 60 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 61 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 62 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 63 
     | 
    
         
            +
              name: minitest
         
     | 
| 
      
 64 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 65 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 66 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 67 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 68 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 69 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 70 
     | 
    
         
            +
              type: :development
         
     | 
| 
      
 71 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 72 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 73 
     | 
    
         
            +
                none: false
         
     | 
| 
      
 74 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 75 
     | 
    
         
            +
                - - ! '>='
         
     | 
| 
      
 76 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 77 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 78 
     | 
    
         
            +
            description: Generate and executed workflows dynamically based on input data and leave
         
     | 
| 
      
 79 
     | 
    
         
            +
              it open for others to jump into it as well
         
     | 
| 
      
 80 
     | 
    
         
            +
            email:
         
     | 
| 
      
 81 
     | 
    
         
            +
            - inecas@redhat.com
         
     | 
| 
      
 82 
     | 
    
         
            +
            executables: []
         
     | 
| 
      
 83 
     | 
    
         
            +
            extensions: []
         
     | 
| 
      
 84 
     | 
    
         
            +
            extra_rdoc_files: []
         
     | 
| 
      
 85 
     | 
    
         
            +
            files:
         
     | 
| 
      
 86 
     | 
    
         
            +
            - .gitignore
         
     | 
| 
      
 87 
     | 
    
         
            +
            - Gemfile
         
     | 
| 
      
 88 
     | 
    
         
            +
            - MIT-LICENSE
         
     | 
| 
      
 89 
     | 
    
         
            +
            - README.md
         
     | 
| 
      
 90 
     | 
    
         
            +
            - Rakefile
         
     | 
| 
      
 91 
     | 
    
         
            +
            - dynflow.gemspec
         
     | 
| 
      
 92 
     | 
    
         
            +
            - examples/events.rb
         
     | 
| 
      
 93 
     | 
    
         
            +
            - examples/workflow.rb
         
     | 
| 
      
 94 
     | 
    
         
            +
            - lib/dynflow.rb
         
     | 
| 
      
 95 
     | 
    
         
            +
            - lib/dynflow/action.rb
         
     | 
| 
      
 96 
     | 
    
         
            +
            - lib/dynflow/bus.rb
         
     | 
| 
      
 97 
     | 
    
         
            +
            - lib/dynflow/dispatcher.rb
         
     | 
| 
      
 98 
     | 
    
         
            +
            - lib/dynflow/logger.rb
         
     | 
| 
      
 99 
     | 
    
         
            +
            - lib/dynflow/message.rb
         
     | 
| 
      
 100 
     | 
    
         
            +
            - lib/dynflow/orch_request.rb
         
     | 
| 
      
 101 
     | 
    
         
            +
            - lib/dynflow/orch_response.rb
         
     | 
| 
      
 102 
     | 
    
         
            +
            - lib/dynflow/version.rb
         
     | 
| 
      
 103 
     | 
    
         
            +
            - test/action_test.rb
         
     | 
| 
      
 104 
     | 
    
         
            +
            - test/bus_test.rb
         
     | 
| 
      
 105 
     | 
    
         
            +
            - test/dispatcher_test.rb
         
     | 
| 
      
 106 
     | 
    
         
            +
            - test/test_helper.rb
         
     | 
| 
      
 107 
     | 
    
         
            +
            homepage: http://github.com/iNecas/eventum
         
     | 
| 
      
 108 
     | 
    
         
            +
            licenses: []
         
     | 
| 
      
 109 
     | 
    
         
            +
            post_install_message: 
         
     | 
| 
      
 110 
     | 
    
         
            +
            rdoc_options: []
         
     | 
| 
      
 111 
     | 
    
         
            +
            require_paths:
         
     | 
| 
      
 112 
     | 
    
         
            +
            - lib
         
     | 
| 
      
 113 
     | 
    
         
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         
     | 
| 
      
 114 
     | 
    
         
            +
              none: false
         
     | 
| 
      
 115 
     | 
    
         
            +
              requirements:
         
     | 
| 
      
 116 
     | 
    
         
            +
              - - ! '>='
         
     | 
| 
      
 117 
     | 
    
         
            +
                - !ruby/object:Gem::Version
         
     | 
| 
      
 118 
     | 
    
         
            +
                  version: '0'
         
     | 
| 
      
 119 
     | 
    
         
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         
     | 
| 
      
 120 
     | 
    
         
            +
              none: false
         
     | 
| 
      
 121 
     | 
    
         
            +
              requirements:
         
     | 
| 
      
 122 
     | 
    
         
            +
              - - ! '>='
         
     | 
| 
      
 123 
     | 
    
         
            +
                - !ruby/object:Gem::Version
         
     | 
| 
      
 124 
     | 
    
         
            +
                  version: '0'
         
     | 
| 
      
 125 
     | 
    
         
            +
            requirements: []
         
     | 
| 
      
 126 
     | 
    
         
            +
            rubyforge_project: 
         
     | 
| 
      
 127 
     | 
    
         
            +
            rubygems_version: 1.8.25
         
     | 
| 
      
 128 
     | 
    
         
            +
            signing_key: 
         
     | 
| 
      
 129 
     | 
    
         
            +
            specification_version: 3
         
     | 
| 
      
 130 
     | 
    
         
            +
            summary: DYNamic workFLOW engine
         
     | 
| 
      
 131 
     | 
    
         
            +
            test_files:
         
     | 
| 
      
 132 
     | 
    
         
            +
            - test/action_test.rb
         
     | 
| 
      
 133 
     | 
    
         
            +
            - test/bus_test.rb
         
     | 
| 
      
 134 
     | 
    
         
            +
            - test/dispatcher_test.rb
         
     | 
| 
      
 135 
     | 
    
         
            +
            - test/test_helper.rb
         
     |