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.
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
+