apollo 1.0.0

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.
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "apollo"
8
+ gem.summary = %Q{A fork of workflow: a finite-state-machine-inspired API for modeling and interacting with what we tend to refer to as 'workflow'.}
9
+ gem.email = "warlickt@operissystems.com"
10
+ gem.homepage = "http://github.com/tekwiz/apollo"
11
+ gem.authors = ["Travis D. Warlick, Jr."]
12
+ # gem.add_development_dependency "rspec", "~> 1.3.0"
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ # TODO rspecs
21
+ # require 'spec/rake/spectask'
22
+ # Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ # spec.libs << 'lib' << 'spec'
24
+ # spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ # end
26
+ #
27
+ # Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ # spec.libs << 'lib' << 'spec'
29
+ # spec.pattern = 'spec/**/*_spec.rb'
30
+ # spec.rcov = true
31
+ # spec.rcov_opts << '--exclude \/Library\/' << '--exclude /\.gem\/'
32
+ # end
33
+
34
+ # task :spec => :check_dependencies
35
+ #
36
+ # task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "Apollo #{version}"
44
+ rdoc.rdoc_files.include('README*', 'MIT-LICENSE', 'LICENSE', 'VERSION')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
47
+
48
+ task :clobber => [:clobber_rcov, :clobber_rdoc]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,60 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{apollo}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Travis D. Warlick, Jr."]
12
+ s.date = %q{2010-04-23}
13
+ s.email = %q{warlickt@operissystems.com}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "LICENSE",
21
+ "MIT-LICENSE",
22
+ "README.markdown",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "apollo.gemspec",
26
+ "lib/apollo.rb",
27
+ "lib/apollo/active_record_instance_methods.rb",
28
+ "lib/apollo/event.rb",
29
+ "lib/apollo/specification.rb",
30
+ "lib/apollo/state.rb",
31
+ "test/couchtiny_example.rb",
32
+ "test/main_test.rb",
33
+ "test/readme_example.rb",
34
+ "test/test_helper.rb",
35
+ "test/without_active_record_test.rb"
36
+ ]
37
+ s.homepage = %q{http://github.com/tekwiz/apollo}
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+ s.require_paths = ["lib"]
40
+ s.rubygems_version = %q{1.3.6}
41
+ s.summary = %q{A fork of workflow: a finite-state-machine-inspired API for modeling and interacting with what we tend to refer to as 'workflow'.}
42
+ s.test_files = [
43
+ "test/couchtiny_example.rb",
44
+ "test/main_test.rb",
45
+ "test/readme_example.rb",
46
+ "test/test_helper.rb",
47
+ "test/without_active_record_test.rb"
48
+ ]
49
+
50
+ if s.respond_to? :specification_version then
51
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
55
+ else
56
+ end
57
+ else
58
+ end
59
+ end
60
+
@@ -0,0 +1,263 @@
1
+ module Apollo
2
+ autoload :Event, 'apollo/event'
3
+ autoload :State, 'apollo/state'
4
+ autoload :Specification, 'apollo/specification'
5
+ autoload :ActiveRecordInstanceMethods, 'apollo/active_record_instance_methods'
6
+
7
+ # The current version
8
+ VERSION = File.read(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'VERSION')).strip
9
+
10
+ class TransitionHalted < Exception
11
+ attr_reader :halted_because
12
+
13
+ def initialize(msg = nil)
14
+ @halted_because = msg
15
+ super msg
16
+ end
17
+ end
18
+
19
+ class NoTransitionAllowed < Exception; end
20
+
21
+ class ApolloError < Exception; end
22
+
23
+ class ApolloDefinitionError < Exception; end
24
+
25
+ module ClassMethods
26
+ attr_reader :apollo_spec
27
+
28
+ def apollo_column(column_name=nil)
29
+ if column_name
30
+ @apollo_state_column_name = column_name.to_sym
31
+ else
32
+ @apollo_state_column_name ||= :current_state
33
+ end
34
+ @apollo_state_column_name
35
+ end
36
+
37
+ def apollo(&specification)
38
+ @apollo_spec = Specification.new(Hash.new, &specification)
39
+ @apollo_spec.states.values.each do |state|
40
+ state_name = state.name
41
+ module_eval do
42
+ define_method "#{state_name}?" do
43
+ state_name == current_state.name
44
+ end
45
+ end
46
+
47
+ state.events.values.each do |event|
48
+ event_name = event.name
49
+ module_eval do
50
+ define_method "#{event_name}!".to_sym do |*args|
51
+ process_event!(event_name, *args)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ module InstanceMethods
60
+ def current_state
61
+ loaded_state = load_apollo_state
62
+ res = spec.states[loaded_state.to_sym] if loaded_state
63
+ res || spec.initial_state
64
+ end
65
+
66
+ def halted?
67
+ @halted
68
+ end
69
+
70
+ def halted_because
71
+ @halted_because
72
+ end
73
+
74
+ def process_event!(name, *args)
75
+ event = current_state.events[name.to_sym]
76
+ raise NoTransitionAllowed.new(
77
+ "There is no event #{name.to_sym} defined for the #{current_state} state") \
78
+ if event.nil?
79
+ # This three member variables are a relict from the old apollo library
80
+ # TODO: refactor some day
81
+ @halted_because = nil
82
+ @halted = false
83
+ @raise_exception_on_halt = false
84
+ return_value = run_action(event.action, *args) || run_action_callback(event.name, *args)
85
+ if @halted
86
+ if @raise_exception_on_halt
87
+ raise @raise_exception_on_halt
88
+ else
89
+ false
90
+ end
91
+ else
92
+ check_transition(event)
93
+ run_on_transition(current_state, spec.states[event.to], name, *args)
94
+ transition(current_state, spec.states[event.to], name, *args)
95
+ return_value
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def check_transition(event)
102
+ # Create a meaningful error message instead of
103
+ # "undefined method `on_entry' for nil:NilClass"
104
+ # Reported by Kyle Burton
105
+ if !spec.states[event.to]
106
+ raise ApolloError.new("Event[#{event.name}]'s " +
107
+ "to[#{event.to}] is not a declared state.")
108
+ end
109
+ end
110
+
111
+ def spec
112
+ c = self.class
113
+ # using a simple loop instead of class_inheritable_accessor to avoid
114
+ # dependency on Rails' ActiveSupport
115
+ until c.apollo_spec || !(c.include? Apollo)
116
+ c = c.superclass
117
+ end
118
+ c.apollo_spec
119
+ end
120
+
121
+ def halt(reason)
122
+ @halted_because = reason
123
+ @halted = true
124
+ @raise_exception_on_halt = false
125
+ end
126
+
127
+ def halt!(reason, exception_klass = TransitionHalted)
128
+ @halted_because = reason
129
+ @halted = true
130
+ if exception_klass.class == Class
131
+ @raise_exception_on_halt = exception_klass.new(reason)
132
+ else
133
+ @raise_exception_on_halt = exception_klass
134
+ end
135
+ @raise_exception_on_halt.set_backtrace(caller)
136
+ end
137
+
138
+ def transition(from, to, name, *args)
139
+ run_on_exit(from, to, name, *args)
140
+ persist_apollo_state to.to_s
141
+ run_on_entry(to, from, name, *args)
142
+ end
143
+
144
+ def run_on_transition(from, to, event, *args)
145
+ instance_exec(from.name, to.name, event, *args, &spec.on_transition_proc) if spec.on_transition_proc
146
+ end
147
+
148
+ def run_action(action, *args)
149
+ instance_exec(*args, &action) if action
150
+ end
151
+
152
+ def run_action_callback(action_name, *args)
153
+ self.send action_name.to_sym, *args if self.respond_to?(action_name.to_sym)
154
+ end
155
+
156
+ def run_on_entry(state, prior_state, triggering_event, *args)
157
+ if state.on_entry
158
+ instance_exec(prior_state.name, triggering_event, *args, &state.on_entry)
159
+ else
160
+ hook_name = "on_#{state}_entry"
161
+ self.send hook_name, prior_state, triggering_event, *args if self.respond_to? hook_name
162
+ end
163
+ end
164
+
165
+ def run_on_exit(state, new_state, triggering_event, *args)
166
+ if state
167
+ if state.on_exit
168
+ instance_exec(new_state.name, triggering_event, *args, &state.on_exit)
169
+ else
170
+ hook_name = "on_#{state}_exit"
171
+ self.send hook_name, new_state, triggering_event, *args if self.respond_to? hook_name
172
+ end
173
+ end
174
+ end
175
+
176
+ # load_apollo_state and persist_apollo_state
177
+ # can be overriden to handle the persistence of the apollo state.
178
+ #
179
+ # Default (non ActiveRecord) implementation stores the current state
180
+ # in a variable.
181
+ #
182
+ # Default ActiveRecord implementation uses a 'apollo_state' database column.
183
+ def load_apollo_state
184
+ @apollo_state if instance_variable_defined? :@apollo_state
185
+ end
186
+
187
+ def persist_apollo_state(new_value)
188
+ @apollo_state = new_value
189
+ end
190
+ end
191
+
192
+ def self.included(klass)
193
+ klass.send :include, InstanceMethods
194
+ klass.extend ClassMethods
195
+ if Object.const_defined?(:ActiveRecord)
196
+ if klass < ActiveRecord::Base
197
+ klass.send :include, ActiveRecordInstanceMethods
198
+ klass.before_validation :write_initial_state
199
+ end
200
+ end
201
+ end
202
+
203
+ # Generates a `dot` graph of the apollo.
204
+ # Prerequisite: the `dot` binary.
205
+ # You can use it in your own Rakefile like this:
206
+ #
207
+ # namespace :doc do
208
+ # desc "Generate a graph of the apollo."
209
+ # task :apollo do
210
+ # Apollo::create_apollo_diagram(Order.new)
211
+ # end
212
+ # end
213
+ #
214
+ # You can influence the placement of nodes by specifying
215
+ # additional meta information in your states and transition descriptions.
216
+ # You can assign higher `doc_weight` value to the typical transitions
217
+ # in your apollo. All other states and transitions will be arranged
218
+ # around that main line. See also `weight` in the graphviz documentation.
219
+ # Example:
220
+ #
221
+ # state :new do
222
+ # event :approve, :to => :approved, :meta => {:doc_weight => 8}
223
+ # end
224
+ #
225
+ #
226
+ # @param klass A class with the Apollo mixin, for which you wish the graphical apollo representation
227
+ # @param [String] target_dir Directory, where to save the dot and the pdf files
228
+ # @param [String] graph_options You can change graph orientation, size etc. See graphviz documentation
229
+ def self.create_apollo_diagram(klass, target_dir, graph_options='rankdir="LR", size="7,11.6", ratio="fill"')
230
+ apollo_name = "#{klass.name.tableize}_apollo"
231
+ fname = File.join(target_dir, "generated_#{apollo_name}")
232
+ File.open("#{fname}.dot", 'w') do |file|
233
+ file.puts %Q|
234
+ digraph #{apollo_name} {
235
+ graph [#{graph_options}];
236
+ node [shape=box];
237
+ edge [len=1];
238
+ |
239
+
240
+ klass.apollo_spec.states.each do |state_name, state|
241
+ file.puts %Q{ #{state.name} [label="#{state.name}"];}
242
+ state.events.each do |event_name, event|
243
+ meta_info = event.meta
244
+ if meta_info[:doc_weight]
245
+ weight_prop = ", weight=#{meta_info[:doc_weight]}"
246
+ else
247
+ weight_prop = ''
248
+ end
249
+ file.puts %Q{ #{state.name} -> #{event.to} [label="#{event_name.to_s.humanize}" #{weight_prop}];}
250
+ end
251
+ end
252
+ file.puts "}"
253
+ file.puts
254
+ end
255
+ `dot -Tpdf -o#{fname}.pdf #{fname}.dot`
256
+ puts "
257
+ Please run the following to open the generated file:
258
+
259
+ open #{fname}.pdf
260
+
261
+ "
262
+ end
263
+ end
@@ -0,0 +1,24 @@
1
+ module Apollo
2
+ module ActiveRecordInstanceMethods
3
+ def load_apollo_state
4
+ read_attribute(self.class.apollo_column)
5
+ end
6
+
7
+ # On transition the new apollo state is immediately saved in the
8
+ # database.
9
+ def persist_apollo_state(new_value)
10
+ update_attribute self.class.apollo_column, new_value
11
+ end
12
+
13
+ private
14
+
15
+ # Motivation: even if NULL is stored in the apollo_state database column,
16
+ # the current_state is correctly recognized in the Ruby code. The problem
17
+ # arises when you want to SELECT records filtering by the value of initial
18
+ # state. That's why it is important to save the string with the name of the
19
+ # initial state in all the new records.
20
+ def write_initial_state
21
+ write_attribute self.class.apollo_column, current_state.to_s
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ module Apollo
2
+ class Event
3
+ attr_accessor :name, :to, :meta, :action
4
+
5
+ def initialize(name, to, meta = {}, &action)
6
+ @name, @to, @meta, @action = name, to.to_sym, meta, action
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ module Apollo
2
+ class Specification
3
+ attr_accessor :states, :initial_state, :meta, :on_transition_proc
4
+
5
+ def initialize(meta = {}, &specification)
6
+ @states = Hash.new
7
+ @meta = meta
8
+ instance_eval(&specification)
9
+ end
10
+
11
+ private
12
+
13
+ def state(name, meta = {:meta => {}}, &events_and_etc)
14
+ # meta[:meta] to keep the API consistent..., gah
15
+ new_state = State.new(name, meta[:meta])
16
+ @initial_state = new_state if @states.empty?
17
+ @states[name.to_sym] = new_state
18
+ @scoped_state = new_state
19
+ instance_eval(&events_and_etc) if events_and_etc
20
+ end
21
+
22
+ def event(name, args = {}, &action)
23
+ target = args[:to] || args[:to]
24
+ raise ApolloDefinitionError.new(
25
+ "missing ':to' in apollo event definition for '#{name}'") \
26
+ if target.nil?
27
+ @scoped_state.events[name.to_sym] =
28
+ Event.new(name, target, (args[:meta] or {}), &action)
29
+ end
30
+
31
+ def on_entry(&proc)
32
+ @scoped_state.on_entry = proc
33
+ end
34
+
35
+ def on_exit(&proc)
36
+ @scoped_state.on_exit = proc
37
+ end
38
+
39
+ def on_transition(&proc)
40
+ @on_transition_proc = proc
41
+ end
42
+ end
43
+ end