bumbleworks 0.0.4
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 +2 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.watchr +89 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +84 -0
- data/LICENSE.txt +22 -0
- data/README.md +160 -0
- data/Rakefile +9 -0
- data/bumbleworks.gemspec +30 -0
- data/doc/GUIDE.md +337 -0
- data/doc/TERMS.md +9 -0
- data/lib/bumbleworks.rb +123 -0
- data/lib/bumbleworks/configuration.rb +182 -0
- data/lib/bumbleworks/hash_storage.rb +13 -0
- data/lib/bumbleworks/participant_registration.rb +19 -0
- data/lib/bumbleworks/process_definition.rb +143 -0
- data/lib/bumbleworks/ruote.rb +64 -0
- data/lib/bumbleworks/storage_adapter.rb +23 -0
- data/lib/bumbleworks/support.rb +20 -0
- data/lib/bumbleworks/task.rb +109 -0
- data/lib/bumbleworks/tree_builder.rb +60 -0
- data/lib/bumbleworks/version.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/app/participants/honey_participant.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/app/participants/molasses_participant.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/config_initializer.rb +4 -0
- data/spec/fixtures/apps/with_default_directories/full_initializer.rb +12 -0
- data/spec/fixtures/apps/with_default_directories/lib/process_definitions/garbage_collector.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_honey.rb +3 -0
- data/spec/fixtures/apps/with_default_directories/lib/process_definitions/make_molasses.rb +6 -0
- data/spec/fixtures/apps/with_specified_directories/config_initializer.rb +5 -0
- data/spec/fixtures/apps/with_specified_directories/specific_directory/definitions/.gitkeep +0 -0
- data/spec/fixtures/apps/with_specified_directories/specific_directory/participants/.gitkeep +0 -0
- data/spec/fixtures/definitions/a_list_of_jams.rb +4 -0
- data/spec/fixtures/definitions/nested_folder/test_nested_process.rb +3 -0
- data/spec/fixtures/definitions/test_process.rb +5 -0
- data/spec/integration/configuration_spec.rb +43 -0
- data/spec/integration/sample_application_spec.rb +45 -0
- data/spec/lib/bumbleworks/configuration_spec.rb +162 -0
- data/spec/lib/bumbleworks/participant_registration_spec.rb +13 -0
- data/spec/lib/bumbleworks/process_definition_spec.rb +178 -0
- data/spec/lib/bumbleworks/ruote_spec.rb +107 -0
- data/spec/lib/bumbleworks/storage_adapter_spec.rb +41 -0
- data/spec/lib/bumbleworks/support_spec.rb +40 -0
- data/spec/lib/bumbleworks/task_spec.rb +274 -0
- data/spec/lib/bumbleworks/tree_builder_spec.rb +95 -0
- data/spec/lib/bumbleworks_spec.rb +133 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/path_helpers.rb +11 -0
- metadata +262 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
require "ruote"
|
2
|
+
|
3
|
+
module Bumbleworks
|
4
|
+
class Ruote
|
5
|
+
class << self
|
6
|
+
def dashboard(options = {})
|
7
|
+
@dashboard ||= begin
|
8
|
+
context = if Bumbleworks.autostart_worker || options[:start_worker] == true
|
9
|
+
::Ruote::Worker.new(storage)
|
10
|
+
else
|
11
|
+
storage
|
12
|
+
end
|
13
|
+
::Ruote::Dashboard.new(context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def start_worker!(options = {})
|
18
|
+
@dashboard = nil
|
19
|
+
dashboard(:start_worker => true)
|
20
|
+
dashboard.join if options[:join] == true
|
21
|
+
dashboard.worker
|
22
|
+
end
|
23
|
+
|
24
|
+
def launch(name, options)
|
25
|
+
dashboard.launch(dashboard.variables[name], options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def register_participants(&block)
|
29
|
+
dashboard.register(&block) if block
|
30
|
+
set_catchall_if_needed
|
31
|
+
dashboard.participant_list
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_catchall_if_needed
|
35
|
+
last_participant = dashboard.participant_list.last
|
36
|
+
unless last_participant && last_participant.regex == "^.+$" && last_participant.classname == "Ruote::StorageParticipant"
|
37
|
+
catchall = ::Ruote::ParticipantEntry.new(["^.+$", ["Ruote::StorageParticipant", {}]])
|
38
|
+
dashboard.participant_list = dashboard.participant_list.push(catchall)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def storage
|
43
|
+
@storage ||= begin
|
44
|
+
all_adapters = Bumbleworks.configuration.storage_adapters
|
45
|
+
adapter = all_adapters.detect do |adapter|
|
46
|
+
adapter.use?(Bumbleworks.storage)
|
47
|
+
end
|
48
|
+
raise UndefinedSetting, "Storage is missing or not supported. Supported: #{all_adapters.map(&:display_name).join(', ')}" unless adapter
|
49
|
+
adapter.driver.new(Bumbleworks.storage)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset!
|
54
|
+
if @storage
|
55
|
+
@storage.purge!
|
56
|
+
@storage.shutdown
|
57
|
+
end
|
58
|
+
@dashboard.shutdown if @dashboard && @dashboard.respond_to?(:shutdown)
|
59
|
+
@storage = nil
|
60
|
+
@dashboard = nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
class StorageAdapter
|
3
|
+
class << self
|
4
|
+
attr_accessor :auto_register
|
5
|
+
|
6
|
+
def auto_register?
|
7
|
+
auto_register.nil? || auto_register == true
|
8
|
+
end
|
9
|
+
|
10
|
+
def driver
|
11
|
+
raise "Subclass responsibility"
|
12
|
+
end
|
13
|
+
|
14
|
+
def use?(storage)
|
15
|
+
storage.class.name =~ /^#{display_name}/
|
16
|
+
end
|
17
|
+
|
18
|
+
def display_name
|
19
|
+
raise "Subclass responsibility"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
# Support methods for utility functionality such as string modification -
|
3
|
+
# could also be accomplished by monkey-patching String class.
|
4
|
+
module Support
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def camelize(string)
|
8
|
+
string = string.sub(/^[a-z\d]*/) { $&.capitalize }
|
9
|
+
string = string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
10
|
+
end
|
11
|
+
|
12
|
+
def all_files(directory, options = {})
|
13
|
+
Dir["#{directory}/**/*.rb"].each do |path|
|
14
|
+
name = File.basename(path, '.rb')
|
15
|
+
name = camelize(name) if options[:camelize] == true
|
16
|
+
yield path, name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
class Task
|
3
|
+
class AlreadyClaimed < StandardError; end
|
4
|
+
class MissingWorkitem < StandardError; end
|
5
|
+
|
6
|
+
extend Forwardable
|
7
|
+
delegate [:sid, :fei, :fields, :params, :participant_name, :wfid, :wf_name] => :@workitem
|
8
|
+
attr_reader :nickname
|
9
|
+
alias_method :id, :sid
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def for_role(identifier)
|
13
|
+
for_roles([identifier])
|
14
|
+
end
|
15
|
+
|
16
|
+
def for_roles(identifiers)
|
17
|
+
return [] unless identifiers.is_a?(Array)
|
18
|
+
workitems = identifiers.collect { |identifier|
|
19
|
+
storage_participant.by_participant(identifier)
|
20
|
+
}.flatten.uniq
|
21
|
+
from_workitems(workitems)
|
22
|
+
end
|
23
|
+
|
24
|
+
def all
|
25
|
+
from_workitems(storage_participant.all)
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_by_id(sid)
|
29
|
+
workitem = storage_participant[sid] if sid
|
30
|
+
raise MissingWorkitem unless workitem
|
31
|
+
new(workitem)
|
32
|
+
rescue ArgumentError => e
|
33
|
+
raise MissingWorkitem, e.message
|
34
|
+
end
|
35
|
+
|
36
|
+
def storage_participant
|
37
|
+
Bumbleworks.dashboard.storage_participant
|
38
|
+
end
|
39
|
+
|
40
|
+
def from_workitems(workitems)
|
41
|
+
workitems.map { |wi|
|
42
|
+
new(wi) if wi.params['task']
|
43
|
+
}.compact
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(workitem)
|
48
|
+
@workitem = workitem
|
49
|
+
unless workitem && workitem.is_a?(::Ruote::Workitem)
|
50
|
+
raise ArgumentError, "Not a valid workitem"
|
51
|
+
end
|
52
|
+
@nickname = params['task']
|
53
|
+
end
|
54
|
+
|
55
|
+
# alias for fields[] (fields delegated to workitem)
|
56
|
+
def [](key)
|
57
|
+
fields[key]
|
58
|
+
end
|
59
|
+
|
60
|
+
# alias for fields[]= (fields delegated to workitem)
|
61
|
+
def []=(key, value)
|
62
|
+
fields[key] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
def role
|
66
|
+
participant_name
|
67
|
+
end
|
68
|
+
|
69
|
+
# update workitem with changes to fields & params
|
70
|
+
def save
|
71
|
+
storage_participant.update(@workitem)
|
72
|
+
end
|
73
|
+
|
74
|
+
# proceed workitem (saving changes to fields)
|
75
|
+
def complete
|
76
|
+
storage_participant.proceed(@workitem)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Claim task and assign token to claim
|
80
|
+
def claim(token)
|
81
|
+
if token && claimant && token != claimant
|
82
|
+
raise AlreadyClaimed, "Already claimed by #{claimant}"
|
83
|
+
end
|
84
|
+
|
85
|
+
params['claimant'] = token
|
86
|
+
save
|
87
|
+
end
|
88
|
+
|
89
|
+
# Token used to claim task, nil if not claimed
|
90
|
+
def claimant
|
91
|
+
params['claimant']
|
92
|
+
end
|
93
|
+
|
94
|
+
# true if task is claimed
|
95
|
+
def claimed?
|
96
|
+
!claimant.nil?
|
97
|
+
end
|
98
|
+
|
99
|
+
# release claim on task.
|
100
|
+
def release
|
101
|
+
claim(nil)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
def storage_participant
|
106
|
+
self.class.storage_participant
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "ruote"
|
2
|
+
require "ruote/reader"
|
3
|
+
|
4
|
+
module Bumbleworks
|
5
|
+
class TreeBuilder
|
6
|
+
class InvalidTree < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :tree, :name
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
@forced_name = options[:name]
|
12
|
+
@definition = options[:definition]
|
13
|
+
@tree = options[:tree]
|
14
|
+
unless !!@definition ^ !!@tree
|
15
|
+
raise ArgumentError, "Must specify either definition or tree (not both)"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build!
|
20
|
+
initialize_tree_from_definition! unless @tree
|
21
|
+
if @name = name_from_tree
|
22
|
+
@forced_name ||= name
|
23
|
+
raise InvalidTree, "Name does not match name in definition" if @forced_name != @name
|
24
|
+
@tree[1].delete(@tree[1].keys.first)
|
25
|
+
end
|
26
|
+
@name = @forced_name
|
27
|
+
add_name_to_tree!
|
28
|
+
@tree
|
29
|
+
rescue ::Ruote::Reader::Error => e
|
30
|
+
raise InvalidTree, e.message
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def from_definition(*args, &block)
|
35
|
+
tree = ::Ruote.define *args, &block
|
36
|
+
builder = new(:tree => tree)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def initialize_tree_from_definition!
|
43
|
+
converted = @definition.strip.gsub(/^Bumbleworks.define_process/, 'Ruote.define')
|
44
|
+
@tree = ::Ruote::Reader.read(converted)
|
45
|
+
end
|
46
|
+
|
47
|
+
def name_from_tree
|
48
|
+
first_key, first_value = @tree[1].first
|
49
|
+
name_from_tree = if first_key == 'name'
|
50
|
+
first_value
|
51
|
+
elsif first_value.nil?
|
52
|
+
first_key
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_name_to_tree!
|
57
|
+
@tree[1] = { "name" => @name }.merge(@tree[1])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,43 @@
|
|
1
|
+
describe Bumbleworks::Configuration do
|
2
|
+
let(:specified_app_path) { File.join(fixtures_path, 'apps', 'with_specified_directories') }
|
3
|
+
let(:default_app_path) { File.join(fixtures_path, 'apps', 'with_default_directories') }
|
4
|
+
let(:specified_initializer) { File.join(specified_app_path, 'config_initializer.rb') }
|
5
|
+
let(:default_initializer) { File.join(default_app_path, 'config_initializer.rb') }
|
6
|
+
|
7
|
+
describe '#root' do
|
8
|
+
it 'returns configured root' do
|
9
|
+
load default_initializer
|
10
|
+
Bumbleworks.root.should == default_app_path
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#definitions_directory' do
|
15
|
+
it 'returns specified directory when set in configuration' do
|
16
|
+
load specified_initializer
|
17
|
+
Bumbleworks.definitions_directory.should == File.join(specified_app_path, 'specific_directory', 'definitions')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns default directory when not set in configuration' do
|
21
|
+
load default_initializer
|
22
|
+
Bumbleworks.definitions_directory.should == File.join(default_app_path, 'lib', 'process_definitions')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#participants_directory' do
|
27
|
+
it 'returns specified directory when set in configuration' do
|
28
|
+
load specified_initializer
|
29
|
+
Bumbleworks.participants_directory.should == File.join(specified_app_path, 'specific_directory', 'participants')
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns default directory when not set in configuration' do
|
33
|
+
load default_initializer
|
34
|
+
Bumbleworks.participants_directory.should == File.join(default_app_path, 'app', 'participants')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns second default directory if first not found' do
|
38
|
+
load default_initializer
|
39
|
+
Bumbleworks.root = File.join(default_app_path, 'app')
|
40
|
+
Bumbleworks.participants_directory.should == File.join(default_app_path, 'app', 'participants')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
describe 'Bumbleworks Sample Application' do
|
2
|
+
describe 'initializer' do
|
3
|
+
let(:app_root) {
|
4
|
+
File.expand_path(File.join(fixtures_path, 'apps', 'with_default_directories'))
|
5
|
+
}
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Bumbleworks.reset!
|
9
|
+
load File.join(app_root, 'full_initializer.rb')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'registers participants' do
|
13
|
+
Bumbleworks.dashboard.participant_list.should have(3).items
|
14
|
+
Bumbleworks.dashboard.participant_list.map(&:classname).should =~ ['HoneyParticipant', 'MolassesParticipant', 'Ruote::StorageParticipant']
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'loads process definitions' do
|
18
|
+
Bumbleworks.dashboard.variables['make_honey'].should == ["define",
|
19
|
+
{"name"=>"make_honey"}, [["dave", {"task"=>"make_some_honey"}, []]]]
|
20
|
+
Bumbleworks.dashboard.variables['garbage_collector'].should == ["define",
|
21
|
+
{"name"=>"garbage_collector"}, [["george", {"ref"=>"garbage collector"}, []]]]
|
22
|
+
|
23
|
+
Bumbleworks.dashboard.variables['make_molasses'].should == ["define",
|
24
|
+
{"name"=>"make_molasses"},
|
25
|
+
[["concurrence",
|
26
|
+
{},
|
27
|
+
[["dave", {"task"=>"make_some_molasses"}, []], ["sam", {"task"=>"taste_that_molasses"}, []]]]]]
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'automatically starts engine and waits for first task in catchall participant' do
|
31
|
+
Bumbleworks.launch!('make_honey')
|
32
|
+
Bumbleworks.dashboard.wait_for(:dave)
|
33
|
+
Bumbleworks::Task.all.should have(1).item
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'automatically starts engine and waits for first task in catchall participant' do
|
37
|
+
Bumbleworks.launch!('make_molasses')
|
38
|
+
Bumbleworks.dashboard.wait_for(:dave)
|
39
|
+
Bumbleworks::Task.all.should have(2).item
|
40
|
+
Bumbleworks::Task.for_role('dave').should have(1).item
|
41
|
+
Bumbleworks::Task.for_role('sam').should have(1).item
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|