bumbleworks 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +2 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/.watchr +89 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +84 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +160 -0
  9. data/Rakefile +9 -0
  10. data/bumbleworks.gemspec +30 -0
  11. data/doc/GUIDE.md +337 -0
  12. data/doc/TERMS.md +9 -0
  13. data/lib/bumbleworks.rb +123 -0
  14. data/lib/bumbleworks/configuration.rb +182 -0
  15. data/lib/bumbleworks/hash_storage.rb +13 -0
  16. data/lib/bumbleworks/participant_registration.rb +19 -0
  17. data/lib/bumbleworks/process_definition.rb +143 -0
  18. data/lib/bumbleworks/ruote.rb +64 -0
  19. data/lib/bumbleworks/storage_adapter.rb +23 -0
  20. data/lib/bumbleworks/support.rb +20 -0
  21. data/lib/bumbleworks/task.rb +109 -0
  22. data/lib/bumbleworks/tree_builder.rb +60 -0
  23. data/lib/bumbleworks/version.rb +3 -0
  24. data/spec/fixtures/apps/with_default_directories/app/participants/honey_participant.rb +3 -0
  25. data/spec/fixtures/apps/with_default_directories/app/participants/molasses_participant.rb +3 -0
  26. data/spec/fixtures/apps/with_default_directories/config_initializer.rb +4 -0
  27. data/spec/fixtures/apps/with_default_directories/full_initializer.rb +12 -0
  28. data/spec/fixtures/apps/with_default_directories/lib/process_definitions/garbage_collector.rb +3 -0
  29. data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_honey.rb +3 -0
  30. data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_molasses.rb +6 -0
  31. data/spec/fixtures/apps/with_specified_directories/config_initializer.rb +5 -0
  32. data/spec/fixtures/apps/with_specified_directories/specific_directory/definitions/.gitkeep +0 -0
  33. data/spec/fixtures/apps/with_specified_directories/specific_directory/participants/.gitkeep +0 -0
  34. data/spec/fixtures/definitions/a_list_of_jams.rb +4 -0
  35. data/spec/fixtures/definitions/nested_folder/test_nested_process.rb +3 -0
  36. data/spec/fixtures/definitions/test_process.rb +5 -0
  37. data/spec/integration/configuration_spec.rb +43 -0
  38. data/spec/integration/sample_application_spec.rb +45 -0
  39. data/spec/lib/bumbleworks/configuration_spec.rb +162 -0
  40. data/spec/lib/bumbleworks/participant_registration_spec.rb +13 -0
  41. data/spec/lib/bumbleworks/process_definition_spec.rb +178 -0
  42. data/spec/lib/bumbleworks/ruote_spec.rb +107 -0
  43. data/spec/lib/bumbleworks/storage_adapter_spec.rb +41 -0
  44. data/spec/lib/bumbleworks/support_spec.rb +40 -0
  45. data/spec/lib/bumbleworks/task_spec.rb +274 -0
  46. data/spec/lib/bumbleworks/tree_builder_spec.rb +95 -0
  47. data/spec/lib/bumbleworks_spec.rb +133 -0
  48. data/spec/spec_helper.rb +20 -0
  49. data/spec/support/path_helpers.rb +11 -0
  50. metadata +262 -0
data/doc/TERMS.md ADDED
@@ -0,0 +1,9 @@
1
+ Process Definitions - a map of the assembly line, which dictates where the activities, gateways, events, etc. will be encountered by the product.
2
+ Process Instances - a trip through the assembly line, taken by a single product
3
+ Activities - actions performed by factory workers or robots on the product - carving, painting, cleaning, filling, soldering, etc.
4
+ Gateways - decision points on where the product should go, when the assembly line branches (e.g. larger widgets go to the Big Widget Cleaning Station, smaller ones go to the Small Widget Cleaning Station)
5
+ Message Events, Schedules - pausing the line for a shift change or lunch break, waiting at a certain stage until a needed part has been delivered, etc.
6
+ Workitem/Payload - the Product
7
+ Worker - turning on the power to the Conveyor Belt
8
+ Workflow Engine - the Factory
9
+ This is an abstract term that encompasses all of the above. It's a term that's often used in comparison with a "state machine."
@@ -0,0 +1,123 @@
1
+ require "forwardable"
2
+ require "bumbleworks/version"
3
+ require "bumbleworks/configuration"
4
+ require "bumbleworks/support"
5
+ require "bumbleworks/process_definition"
6
+ require "bumbleworks/task"
7
+ require "bumbleworks/participant_registration"
8
+ require "bumbleworks/ruote"
9
+ require "bumbleworks/hash_storage"
10
+
11
+ module Bumbleworks
12
+ class UnsupportedMode < StandardError; end
13
+ class UndefinedSetting < StandardError; end
14
+ class InvalidSetting < StandardError; end
15
+
16
+ class << self
17
+ extend Forwardable
18
+ attr_accessor :env
19
+
20
+ Configuration.defined_settings.each do |setting|
21
+ def_delegators :configuration, setting, "#{setting.to_s}="
22
+ end
23
+
24
+ def_delegators Bumbleworks::Ruote, :dashboard, :start_worker!
25
+ def_delegator Bumbleworks::ProcessDefinition, :define, :define_process
26
+
27
+ # @public
28
+ # Returns the global configuration, or initializes a new
29
+ # configuration object if it doesn't exist yet.
30
+ def configuration
31
+ @configuration ||= begin
32
+ configuration = Bumbleworks::Configuration.new
33
+ configuration.add_storage_adapter(Bumbleworks::HashStorage)
34
+ if defined?(Bumbleworks::Redis::Adapter) && Bumbleworks::Redis::Adapter.auto_register?
35
+ configuration.add_storage_adapter(Bumbleworks::Redis::Adapter)
36
+ end
37
+ if defined?(Bumbleworks::Sequel::Adapter) && Bumbleworks::Sequel::Adapter.auto_register?
38
+ configuration.add_storage_adapter(Bumbleworks::Sequel::Adapter)
39
+ end
40
+ configuration
41
+ end
42
+ end
43
+
44
+ # @public
45
+ # Yields the global configurtion to a block.
46
+ # @yield [configuration] global configuration
47
+ #
48
+ # @example
49
+ # Bumbleworks.configure do |c|
50
+ # c.root = 'path/to/ruote/assets'
51
+ # end
52
+ # @see Bumbleworks::Configuration
53
+ def configure(&block)
54
+ unless block
55
+ raise ArgumentError.new("You tried to .configure without a block!")
56
+ end
57
+ yield configuration
58
+ end
59
+
60
+ # @public
61
+ # Same as .configure, but clears all existing configuration
62
+ # settings first.
63
+ # @yield [configuration] global configuration
64
+ # @see Bumbleworks.configure
65
+ def configure!(&block)
66
+ @configuration = nil
67
+ configure(&block)
68
+ end
69
+
70
+ # @public
71
+ # Accepts a block for registering participants which
72
+ # is envoked when start! is called. Notice that a
73
+ # 'catchall' storage participant is always added to
74
+ # the end of the list (unless it is defined in the block).
75
+ #
76
+ # @example
77
+ # Bumbleworks.register_participants do
78
+ # painter PainterClass
79
+ # builder BuilderClass
80
+ # plumber PlumberClass
81
+ # end
82
+ def register_participants(&block)
83
+ @participant_block = block
84
+ end
85
+
86
+ # @public
87
+ # Starts a Ruote engine, sets up the storage and registers participants
88
+ # and process_definitions with the Ruote engine.
89
+ def start!
90
+ autoload_and_register_participants
91
+ load_process_definitions
92
+ end
93
+
94
+ # @public
95
+ # Resets Bumbleworks - clears configuration and setup variables, and
96
+ # shuts down the dashboard.
97
+ def reset!
98
+ @configuration = nil
99
+ @participant_block = nil
100
+ @registered_process_definitions = nil
101
+ Bumbleworks::Ruote.reset!
102
+ end
103
+
104
+ # @public
105
+ # Launches the workflow engine with the specified process name.
106
+ # The process_definiton_name should already be registered with
107
+ # Bumbleworks.
108
+ def launch!(process_definition_name, options = {})
109
+ Bumbleworks::Ruote.launch(process_definition_name, options)
110
+ end
111
+
112
+ private
113
+
114
+ def autoload_and_register_participants
115
+ Bumbleworks::ParticipantRegistration.autoload_all
116
+ Bumbleworks::Ruote.register_participants(&@participant_block)
117
+ end
118
+
119
+ def load_process_definitions
120
+ Bumbleworks::ProcessDefinition.create_all_from_directory!(definitions_directory)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,182 @@
1
+ module Bumbleworks
2
+ # Stores configruation information
3
+ #
4
+ # Configruation inforamtion is loaded from a configuration block defined within
5
+ # the client application.
6
+ #
7
+ # @example Standard settings
8
+ # Bumbleworks.configure do |c|
9
+ # c.definitions_directory = '/path/to/ruote/definitions/directory'
10
+ # c.storage = Redis.new(:host => '127.0.0.1', :db => 0, :thread_safe => true)
11
+ # # ...
12
+ # end
13
+ #
14
+ class Configuration
15
+ attr_reader :storage_adapters
16
+
17
+ class << self
18
+ def define_setting(name)
19
+ defined_settings << name
20
+ attr_accessor name
21
+ end
22
+
23
+ def defined_settings
24
+ @defined_settings ||= []
25
+ end
26
+ end
27
+
28
+
29
+ # Path to the root folder where Bumbleworks assets can be found.
30
+ # This includes the following structure:
31
+ # /lib
32
+ # /process_definitions
33
+ # /participants
34
+ # /app/participants
35
+ #
36
+ # default: non, must be specified
37
+ # Exceptions: raises Bumbleworks::UndefinedSetting if not defined by the client
38
+ #
39
+ define_setting :root
40
+
41
+ # Path to the folder which holds the ruote definition files. Bumbleworks
42
+ # will autoload all definition files by recursively traversing the directory
43
+ # tree under this folder. No specific loading order is guranteed
44
+ #
45
+ # default: ${Bumbleworks.root}/lib/process_definitions
46
+ define_setting :definitions_directory
47
+
48
+ # Path to the folder which holds the ruote participant files. Bumbleworks
49
+ # will autoload all participant files by recursively traversing the directory
50
+ # tree under this folder. No specific loading order is guranteed
51
+ #
52
+ # Bumbleworks will guarantee that these files are autoloaded before registration
53
+ # of participants.
54
+ #
55
+ # default: ${Bumbleworks.root}/participants then ${Bumbleworks.root}/app/participants
56
+ define_setting :participants_directory
57
+
58
+ # Bumbelworks requires a dedicated key-value storage for process information. Two
59
+ # storage solutions are currently supported: Redis and Sequel. You can set the storage
60
+ # as follows:
61
+ #
62
+ # @Exammple: Redis
63
+ # Bumbleworks.storage = Redis.new(:host => '127.0.0.1', :db => 0, :thread_safe => true)
64
+ #
65
+ # @Example: Sequel with Postgres db
66
+ # Bumbleworks.storage = Sequel.connect('postgres://user:password@host:port/database_name')
67
+ #
68
+ define_setting :storage
69
+
70
+ # By default, a worker will NOT be started when the storage is initialized;
71
+ # this is the recommended practice since workers should be instantiated in
72
+ # their own threads, and multiple workers (even on different hosts) can run
73
+ # simultaneously. However, if you want a worker to start up immediately
74
+ # (useful for testing or development), set autostart_worker to true.
75
+ #
76
+ # default: false
77
+ define_setting :autostart_worker
78
+
79
+ def initialize
80
+ @storage_adapters = []
81
+ end
82
+
83
+ # Path where Bumbleworks will look for ruote process defintiions to load.
84
+ # The path can be relative or absolute. Relative paths are
85
+ # relative to Bumbleworks.root.
86
+ #
87
+ def definitions_directory
88
+ @definitions_folder ||= default_definition_directory
89
+ end
90
+
91
+ # Path where Bumbleworks will look for ruote participants to load.
92
+ # The path can be relative or absolute. Relative paths are
93
+ # relative to Bumbleworks.root.
94
+ #
95
+ def participants_directory
96
+ @participants_folder ||= default_participant_directory
97
+ end
98
+
99
+ # Root folder where bumbleworks looks for ruote assets (participants,
100
+ # process_definitions, ..etc.) The root path must be absolute.
101
+ # It can be defined throguh a configuration block:
102
+ # Bumbleworks.configure {|c| c.root = '/somewhere'}
103
+ #
104
+ # Or directly:
105
+ # Bumbleworks.root = '/somewhere/else/'
106
+ #
107
+ # If the root is not defined, Bumbleworks will use the root of known
108
+ # frameworks (Rails, Sinatra and Rory). Otherwise, it will raise an
109
+ # error if not defined.
110
+ #
111
+ def root
112
+ @root ||= case
113
+ when defined?(Rails) then Rails.root
114
+ when defined?(Rory) then Rory.root
115
+ when defined?(Sinatra::Application) then Sinatra::Application.root
116
+ else
117
+ raise UndefinedSetting.new("Bumbleworks.root must be set") unless @root
118
+ end
119
+ end
120
+
121
+ # Whether or not we should start a worker when initializing the dashboard
122
+ # and storage. Only returns true if set explicitly to true.
123
+ #
124
+ def autostart_worker
125
+ @autostart_worker == true
126
+ end
127
+
128
+ # Add a storage adapter to the set of possible adapters. Takes an object
129
+ # that responds to `driver`, `use?(storage)`, and `display_name`.
130
+ #
131
+ def add_storage_adapter(storage_adapter)
132
+ raise ArgumentError, "#{storage_adapter} is not a Bumbleworks storage adapter" unless
133
+ storage_adapter.respond_to?(:driver) &&
134
+ storage_adapter.respond_to?(:use?) &&
135
+ storage_adapter.respond_to?(:display_name)
136
+
137
+ @storage_adapters << storage_adapter
138
+ @storage_adapters
139
+ end
140
+
141
+ # Clears all memoize variables and configuration settings
142
+ #
143
+ def clear!
144
+ defined_settings.each {|setting| instance_variable_set("@#{setting}", nil)}
145
+ @storage_adapters = []
146
+ @definitions_folder = @participants_folder = nil
147
+ end
148
+
149
+ private
150
+ def defined_settings
151
+ self.class.defined_settings
152
+ end
153
+
154
+ def default_definition_directory
155
+ default_folders = ['lib/process_definitions']
156
+ find_folder(default_folders, @definitions_directory, "Definitions folder not found")
157
+ end
158
+
159
+ def default_participant_directory
160
+ default_folders = ['participants', 'app/participants']
161
+ find_folder(default_folders, @participants_directory, "Participants folder not found")
162
+ end
163
+
164
+ def find_folder(default_directories, defined_directory, message)
165
+ # use defined directory structure if defined
166
+ if defined_directory
167
+ defined_directory = File.join(root, defined_directory) unless defined_directory[0] == '/'
168
+ end
169
+
170
+ # next look in default directory structure
171
+ defined_directory ||= default_directories.detect do |default_folder|
172
+ folder = File.join(root, default_folder)
173
+ next unless File.directory?(folder)
174
+ break folder
175
+ end
176
+
177
+ return defined_directory if File.directory?(defined_directory.to_s)
178
+
179
+ raise Bumbleworks::InvalidSetting, "#{message}: #{defined_directory}"
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,13 @@
1
+ require 'bumbleworks/storage_adapter'
2
+
3
+ module Bumbleworks
4
+ class HashStorage < Bumbleworks::StorageAdapter
5
+ def self.driver
6
+ ::Ruote::HashStorage
7
+ end
8
+
9
+ def self.display_name
10
+ 'Hash'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Bumbleworks
2
+ class ParticipantRegistration
3
+ class << self
4
+ # @public
5
+ # Autoload all participant classes defined in files in the
6
+ # participants_directory. The symbol for autoload comes from the
7
+ # camelized version of the filename, so this method is dependent on
8
+ # following that convention. For example, file `goat_challenge.rb`
9
+ # should define `GoatChallenge`.
10
+ #
11
+ def autoload_all(options = {})
12
+ options[:directory] ||= Bumbleworks.participants_directory
13
+ Bumbleworks::Support.all_files(options[:directory], :camelize => true) do |path, name|
14
+ Object.autoload name.to_sym, path
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,143 @@
1
+ require "bumbleworks/tree_builder"
2
+
3
+ module Bumbleworks
4
+ class ProcessDefinition
5
+ class NotFound < StandardError; end
6
+ class FileNotFound < StandardError; end
7
+ class Invalid < StandardError; end
8
+
9
+ attr_reader :name, :definition, :tree
10
+
11
+ # @public
12
+ # Initialize a new ProcessDefinition, supplying a name (required), and
13
+ # definition or a tree (one of which is required). The definition should
14
+ # be a string with a Bumbleworks.define_process block, and the tree should
15
+ # be an actual Ruote tree.
16
+ #
17
+ def initialize(opts = {})
18
+ @name = opts[:name]
19
+ @definition = opts[:definition]
20
+ @tree = opts[:tree]
21
+ end
22
+
23
+ # @public
24
+ # Validates the ProcessDefinition by checking existence and uniqueness of
25
+ # name, existence of one of definition or tree, and validity of definition.
26
+ # Raises a Bumbleworks::ProcessDefinition::Invalid exception if errors are
27
+ # found, otherwise returns true
28
+ #
29
+ def validate!
30
+ errors = []
31
+ errors << "Name must be specified" unless @name
32
+ errors << "Definition or tree must be specified" unless @definition || @tree
33
+ errors << "Name is not unique" if Bumbleworks.dashboard.variables[@name]
34
+ begin
35
+ build_tree!
36
+ rescue Invalid
37
+ errors << "Definition is not a valid process definition"
38
+ end
39
+ raise Invalid, "Validation failed: #{errors.join(', ')}" unless errors.empty?
40
+ true
41
+ end
42
+
43
+ # @public
44
+ # Validates first, then adds the tree (builds it if necessary) to the
45
+ # dashboard's variables.
46
+ #
47
+ def save!
48
+ validate!
49
+ Bumbleworks.dashboard.variables[@name] = @tree || build_tree!
50
+ self
51
+ end
52
+
53
+ # @public
54
+ # Uses the TreeBuilder to construct a tree from the given definition.
55
+ # Captures any tree building exceptions and reraises them as
56
+ # Bumbleworks::ProcessDefinition::Invalid exceptions.
57
+ #
58
+ def build_tree!
59
+ return nil unless @definition
60
+ @tree = Bumbleworks::TreeBuilder.new(
61
+ :name => name, :definition => definition
62
+ ).build!
63
+ rescue Bumbleworks::TreeBuilder::InvalidTree => e
64
+ raise Invalid, e.message
65
+ end
66
+
67
+ # @public
68
+ # A definition can be loaded directly from a file (this is the easiest way
69
+ # to do it, after the .create_all_from_directory! method). Simply reads
70
+ # the file at the given path, and set this instance's definition to the
71
+ # contents of that file.
72
+ #
73
+ def load_definition_from_file(path)
74
+ if File.exists?(path)
75
+ @definition = File.read(path)
76
+ else
77
+ raise FileNotFound, "No file found at #{path}"
78
+ end
79
+ end
80
+
81
+ class << self
82
+ # @public
83
+ # Given a key, will instantiate a new ProcessDefinition populated with the
84
+ # tree found in the dashboard variables at that key. If the key isn't
85
+ # found, an exception is thrown.
86
+ #
87
+ def find_by_name(search_key)
88
+ if saved_tree = Bumbleworks.dashboard.variables[search_key]
89
+ new(:name => search_key, :tree => saved_tree)
90
+ else
91
+ raise NotFound, "No definition by the name of \"#{search_key}\" has been registered yet"
92
+ end
93
+ end
94
+
95
+ # @public
96
+ # This method provides a way to define a process definition directly,
97
+ # without having to create it as a string definition or a tree. It takes
98
+ # a block identical to Ruote.define's block, normalizes the definition's
99
+ # name, and `#create!`s a ProcessDefinition with the resulting tree.
100
+ #
101
+ def define(*args, &block)
102
+ tree_builder = Bumbleworks::TreeBuilder.from_definition(*args, &block)
103
+ tree_builder.build!
104
+ create!(:tree => tree_builder.tree, :name => tree_builder.name)
105
+ end
106
+
107
+ # @public
108
+ # Instantiates a new ProcessDefinition, then `#save`s it.
109
+ #
110
+ def create!(opts)
111
+ pdef = new(opts)
112
+ pdef.save!
113
+ pdef
114
+ end
115
+
116
+ # @public
117
+ # For every *.rb file in the given directory (recursive), creates a new
118
+ # ProcessDefinition, reading the file's contents for the definition
119
+ # string. If the :skip_invalid option is specified, all invalid
120
+ # definitions are skipped, and everything else is loaded. Otherwise, the
121
+ # first invalid definition found triggers a rollback and raises the
122
+ # exception.
123
+ #
124
+ def create_all_from_directory!(directory, opts = {})
125
+ added_names = []
126
+ Bumbleworks::Support.all_files(directory) do |path, basename|
127
+ puts "Registering process definition #{basename} from file #{path}" if opts[:verbose] == true
128
+ begin
129
+ create!(:name => basename, :definition => File.read(path))
130
+ added_names << basename
131
+ rescue Invalid
132
+ raise unless opts[:skip_invalid] == true
133
+ end
134
+ end
135
+ rescue Invalid
136
+ added_names.each do |name|
137
+ Bumbleworks.dashboard.variables[name] = nil
138
+ end
139
+ raise
140
+ end
141
+ end
142
+ end
143
+ end