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.
- data/.gitignore +21 -0
- data/LICENSE +201 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +437 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/apollo.gemspec +60 -0
- data/lib/apollo.rb +263 -0
- data/lib/apollo/active_record_instance_methods.rb +24 -0
- data/lib/apollo/event.rb +9 -0
- data/lib/apollo/specification.rb +43 -0
- data/lib/apollo/state.rb +17 -0
- data/test/couchtiny_example.rb +46 -0
- data/test/main_test.rb +418 -0
- data/test/readme_example.rb +37 -0
- data/test/test_helper.rb +16 -0
- data/test/without_active_record_test.rb +54 -0
- metadata +83 -0
data/Rakefile
ADDED
@@ -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
|
data/apollo.gemspec
ADDED
@@ -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
|
+
|
data/lib/apollo.rb
ADDED
@@ -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
|
data/lib/apollo/event.rb
ADDED
@@ -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
|