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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Bumbleworks
2
+ VERSION = "0.0.4"
3
+ end
@@ -0,0 +1,4 @@
1
+ Bumbleworks.configure! do |c|
2
+ c.root = File.dirname(__FILE__)
3
+ c.storage = {}
4
+ end
@@ -0,0 +1,12 @@
1
+ Bumbleworks.configure! do |c|
2
+ c.root = File.dirname(__FILE__)
3
+ c.storage = {}
4
+ c.autostart_worker = true
5
+ end
6
+
7
+ Bumbleworks.register_participants do
8
+ honey_maker HoneyParticipant
9
+ molasses_maker MolassesParticipant
10
+ end
11
+
12
+ Bumbleworks.start!
@@ -0,0 +1,3 @@
1
+ Bumbleworks.define_process 'garbage_collector' do
2
+ george :ref => 'garbage collector'
3
+ end
@@ -0,0 +1,3 @@
1
+ Bumbleworks.define_process 'make_honey' do
2
+ dave :task => 'make_some_honey'
3
+ end
@@ -0,0 +1,6 @@
1
+ Bumbleworks.define_process 'make_molasses' do
2
+ concurrence do
3
+ dave :task => 'make_some_molasses'
4
+ sam :task => 'taste_that_molasses'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ Bumbleworks.configure! do |c|
2
+ c.root = File.dirname(__FILE__)
3
+ c.definitions_directory = 'specific_directory/definitions'
4
+ c.participants_directory = 'specific_directory/participants'
5
+ end
@@ -0,0 +1,4 @@
1
+ raise "Haha I tricked you"
2
+ Raspberry
3
+ Traffic
4
+ Def
@@ -0,0 +1,3 @@
1
+ Bumbleworks.define_process do
2
+ nothing_nested
3
+ end
@@ -0,0 +1,5 @@
1
+
2
+ Bumbleworks.define_process 'test_process' do
3
+ nothing
4
+ end
5
+
@@ -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
+