juice10-action_flow 0.1.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/.document +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +20 -0
- data/LICENSE.txt +20 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +206 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/action_flow.gemspec +149 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/action_flow.rb +59 -0
- data/lib/action_flow/blank_slate.rb +33 -0
- data/lib/action_flow/expression.rb +135 -0
- data/lib/action_flow/filters.rb +25 -0
- data/lib/action_flow/flow.rb +54 -0
- data/lib/action_flow/flow/controller.rb +91 -0
- data/lib/action_flow/flow/state.rb +73 -0
- data/lib/action_flow/helpers.rb +15 -0
- data/lib/action_flow/variable.rb +14 -0
- data/test/helper.rb +18 -0
- data/test/test_action_flow.rb +7 -0
- data/test_app/.gitignore +4 -0
- data/test_app/Gemfile +34 -0
- data/test_app/Gemfile.lock +131 -0
- data/test_app/README +256 -0
- data/test_app/Rakefile +7 -0
- data/test_app/app/controllers/application_controller.rb +6 -0
- data/test_app/app/controllers/settings_controller.rb +33 -0
- data/test_app/app/helpers/application_helper.rb +4 -0
- data/test_app/app/views/layouts/application.html.erb +14 -0
- data/test_app/app/views/settings/_step.html.erb +22 -0
- data/test_app/app/views/settings/intro.html.erb +12 -0
- data/test_app/app/views/settings/outro.html.erb +8 -0
- data/test_app/config.ru +4 -0
- data/test_app/config/application.rb +42 -0
- data/test_app/config/boot.rb +6 -0
- data/test_app/config/cucumber.yml +8 -0
- data/test_app/config/database.yml +25 -0
- data/test_app/config/environment.rb +5 -0
- data/test_app/config/environments/cucumber.rb +35 -0
- data/test_app/config/environments/development.rb +26 -0
- data/test_app/config/environments/production.rb +49 -0
- data/test_app/config/environments/test.rb +35 -0
- data/test_app/config/initializers/backtrace_silencers.rb +7 -0
- data/test_app/config/initializers/inflections.rb +10 -0
- data/test_app/config/initializers/mime_types.rb +5 -0
- data/test_app/config/initializers/secret_token.rb +7 -0
- data/test_app/config/initializers/session_store.rb +8 -0
- data/test_app/config/locales/en.yml +5 -0
- data/test_app/config/routes.rb +58 -0
- data/test_app/db/schema.rb +15 -0
- data/test_app/db/seeds.rb +7 -0
- data/test_app/features/flow_management.feature +122 -0
- data/test_app/features/flow_with_array_alternatives.feature +13 -0
- data/test_app/features/flows_with_conditions.feature +60 -0
- data/test_app/features/mutual_exclusion.feature +39 -0
- data/test_app/features/step_definitions/flow_steps.rb +20 -0
- data/test_app/features/step_definitions/url_steps.rb +7 -0
- data/test_app/features/step_definitions/web_steps.rb +211 -0
- data/test_app/features/support/env.rb +39 -0
- data/test_app/features/support/hooks.rb +3 -0
- data/test_app/features/support/paths.rb +33 -0
- data/test_app/features/support/selectors.rb +39 -0
- data/test_app/lib/tasks/.gitkeep +0 -0
- data/test_app/lib/tasks/cucumber.rake +57 -0
- data/test_app/public/404.html +26 -0
- data/test_app/public/422.html +26 -0
- data/test_app/public/500.html +26 -0
- data/test_app/public/favicon.ico +0 -0
- data/test_app/public/images/rails.png +0 -0
- data/test_app/public/index.html +239 -0
- data/test_app/public/javascripts/application.js +2 -0
- data/test_app/public/javascripts/controls.js +965 -0
- data/test_app/public/javascripts/dragdrop.js +974 -0
- data/test_app/public/javascripts/effects.js +1123 -0
- data/test_app/public/javascripts/prototype.js +6001 -0
- data/test_app/public/javascripts/rails.js +191 -0
- data/test_app/public/robots.txt +5 -0
- data/test_app/public/stylesheets/.gitkeep +0 -0
- data/test_app/script/about +4 -0
- data/test_app/script/console +3 -0
- data/test_app/script/cucumber +10 -0
- data/test_app/script/dbconsole +3 -0
- data/test_app/script/destroy +3 -0
- data/test_app/script/generate +3 -0
- data/test_app/script/performance/benchmarker +3 -0
- data/test_app/script/performance/profiler +3 -0
- data/test_app/script/plugin +3 -0
- data/test_app/script/rails +6 -0
- data/test_app/script/runner +3 -0
- data/test_app/script/server +3 -0
- data/test_app/test/performance/browsing_test.rb +9 -0
- data/test_app/test/test_helper.rb +13 -0
- data/test_app/vendor/plugins/.gitkeep +0 -0
- data/test_app/vendor/plugins/action_flow/init.rb +2 -0
- data/uninstall.rb +1 -0
- metadata +224 -0
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/lib/action_flow'
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
data/lib/action_flow.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
%w[ blank_slate
|
2
|
+
expression
|
3
|
+
variable
|
4
|
+
flow
|
5
|
+
flow/controller
|
6
|
+
flow/state
|
7
|
+
helpers
|
8
|
+
filters
|
9
|
+
].each do |file|
|
10
|
+
require File.dirname(__FILE__) + '/action_flow/' + file
|
11
|
+
end
|
12
|
+
|
13
|
+
module ActionFlow
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def flows
|
17
|
+
@flows ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(&block)
|
21
|
+
DSL.new(self, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class DSL < BlankSlate
|
26
|
+
include Expression::Generator
|
27
|
+
|
28
|
+
def initialize(flow_register, &block)
|
29
|
+
@flow_register = flow_register
|
30
|
+
instance_eval(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def flow(name, *expressions)
|
34
|
+
@flow_register.flows[name.to_sym] = Flow.new(expressions)
|
35
|
+
end
|
36
|
+
|
37
|
+
def mutex(*names)
|
38
|
+
names.each do |name|
|
39
|
+
next unless flow = @flow_register.flows[name]
|
40
|
+
flow.mutexes = names - [name]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def terminate(*args)
|
45
|
+
expression_options = args.pop
|
46
|
+
|
47
|
+
unless expression_options.respond_to?(:[]) && expression = expression_options[:on]
|
48
|
+
raise ArgumentError, 'Could not find :on => expression in terminate'
|
49
|
+
end
|
50
|
+
|
51
|
+
args.each do |arg|
|
52
|
+
flow_name = arg.to_sym
|
53
|
+
next unless @flow_register.flows[flow_name]
|
54
|
+
@flow_register.flows[flow_name].terminators << expression
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActionFlow
|
2
|
+
class BlankSlate
|
3
|
+
|
4
|
+
def self.include(mixin)
|
5
|
+
super(mixin)
|
6
|
+
@mixins ||= []
|
7
|
+
@mixins << mixin
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.new(*args, &block)
|
11
|
+
remove_inherited_methods
|
12
|
+
super(*args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.remove_inherited_methods
|
16
|
+
modules = [self] + (@mixins || [])
|
17
|
+
|
18
|
+
keepers = modules.inject([]) { |list, klass| list + all_methods(klass) } +
|
19
|
+
[:__id__, :__send__, :instance_eval]
|
20
|
+
|
21
|
+
undef_method *( all_methods(self, true) - keepers )
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.all_methods(klass, include_ancestors = false)
|
25
|
+
%w[public protected private].map { |viz|
|
26
|
+
klass.__send__("#{viz}_instance_methods", include_ancestors)
|
27
|
+
}.
|
28
|
+
flatten.map { |m| m.to_sym }.uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module ActionFlow
|
2
|
+
class Expression
|
3
|
+
|
4
|
+
def initialize(controller, params, &block)
|
5
|
+
@params = params.dup
|
6
|
+
@controller = (@params.delete(:controller) || controller).to_s
|
7
|
+
@action = @params.delete(:action).to_s if @params[:action]
|
8
|
+
@format = @params.delete(:format).to_s if @params[:format]
|
9
|
+
@matcher = block
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_hash(hash)
|
13
|
+
new(nil, hash)
|
14
|
+
end
|
15
|
+
|
16
|
+
def +(expression)
|
17
|
+
self.class::Group.new + self + expression
|
18
|
+
end
|
19
|
+
|
20
|
+
def *(expression)
|
21
|
+
@format = expression.instance_eval { @controller }
|
22
|
+
@action = nil
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def /(expression)
|
27
|
+
expression.nesting = @controller
|
28
|
+
expression
|
29
|
+
end
|
30
|
+
|
31
|
+
def nesting=(name)
|
32
|
+
@controller = "#{ name }/#{ @controller }"
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(name, params = {}, &block)
|
36
|
+
@format = name.to_s if @action
|
37
|
+
@action ||= name.to_s
|
38
|
+
@params.update(params) if Hash === params
|
39
|
+
@matcher = block if block_given?
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def verb=(verb)
|
44
|
+
@verb = verb.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def ===(context)
|
48
|
+
p, req = context.params, context.request
|
49
|
+
|
50
|
+
return false if (@controller != p[:controller].to_s) or
|
51
|
+
(@action and @action != p[:action].to_s) or
|
52
|
+
(@verb and !req.__send__("#{ @verb }?")) or
|
53
|
+
(@format and @format != p[:format].to_s)
|
54
|
+
|
55
|
+
return false unless @params.all? do |key, value|
|
56
|
+
Variable === value ||
|
57
|
+
(value == p[key]) || (value == p[key].to_s) ||
|
58
|
+
(value === p[key]) || (value === p[key].to_i)
|
59
|
+
end
|
60
|
+
|
61
|
+
@matcher ? context.instance_eval(&@matcher) : true
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_params(env = {})
|
65
|
+
options = {:controller => '/' + @controller}
|
66
|
+
options[:action] = @action || :index
|
67
|
+
options[:format] = @format if @format
|
68
|
+
|
69
|
+
@params.each do |key, value|
|
70
|
+
value = value.lookup(env) if Variable === value
|
71
|
+
options[key] = value
|
72
|
+
end
|
73
|
+
|
74
|
+
options
|
75
|
+
end
|
76
|
+
|
77
|
+
def inspect
|
78
|
+
source = @controller.dup
|
79
|
+
source << ".#{ @action }" if @action
|
80
|
+
source << "(#{ @params.map { |k,v| ":#{k} => #{v.inspect}" } * ', ' })" unless @params.empty?
|
81
|
+
source << "#{ @action ? '.' : '*' }#{ @format }" if @format
|
82
|
+
source = "#{ @verb }(#{ source })" if @verb
|
83
|
+
source
|
84
|
+
end
|
85
|
+
|
86
|
+
class Group
|
87
|
+
include Enumerable
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
@exprs = []
|
91
|
+
end
|
92
|
+
|
93
|
+
def each(&block)
|
94
|
+
@exprs.each(&block)
|
95
|
+
end
|
96
|
+
|
97
|
+
def +(expression)
|
98
|
+
Enumerable === expression ?
|
99
|
+
expression.each { |exp| @exprs << exp } :
|
100
|
+
@exprs << expression
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def verb=(verb)
|
105
|
+
each { |exp| exp.verb = verb }
|
106
|
+
end
|
107
|
+
|
108
|
+
def ===(context)
|
109
|
+
any? { |exp| exp === context }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
module Generator
|
114
|
+
def method_missing(name, params = {}, &block)
|
115
|
+
Expression.new(name, params, &block)
|
116
|
+
end
|
117
|
+
|
118
|
+
%w[get post put head delete].each do |verb|
|
119
|
+
module_eval <<-EOS
|
120
|
+
def #{verb}(*exprs, &block)
|
121
|
+
group = exprs.inject { |grp, exp| grp + exp }
|
122
|
+
group.verb = :#{verb}
|
123
|
+
group
|
124
|
+
end
|
125
|
+
EOS
|
126
|
+
end
|
127
|
+
|
128
|
+
def find(symbol)
|
129
|
+
Variable.new(symbol)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module ActionFlow
|
4
|
+
module Filters
|
5
|
+
|
6
|
+
include Helpers
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
klass.before_filter :update_flow_status
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def flow(name = nil)
|
15
|
+
flow_state = name.nil? ? flow_controller.current_flow : flow_controller.status[name]
|
16
|
+
flow_state.variables
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_flow_status
|
20
|
+
flow_controller.update_session!
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActionFlow
|
2
|
+
class Flow
|
3
|
+
|
4
|
+
attr_accessor :mutexes, :terminators
|
5
|
+
|
6
|
+
def initialize(expressions)
|
7
|
+
@expressions = expressions
|
8
|
+
@mutexes = []
|
9
|
+
@terminators = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def length
|
13
|
+
@expressions.length
|
14
|
+
end
|
15
|
+
|
16
|
+
def begins_with?(context)
|
17
|
+
match_at?(0, context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def match_at?(index, context, exact = false)
|
21
|
+
return false unless expression = @expressions[index]
|
22
|
+
match_expression?(expression, context, exact)
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_expression?(expression, context, exact = false)
|
26
|
+
return expression.any? { |atom| match_expression?(atom, context, exact) } if Array === expression
|
27
|
+
return ActionFlow.flows[expression] === context if Symbol === expression and not exact
|
28
|
+
expression === context
|
29
|
+
end
|
30
|
+
|
31
|
+
def ===(context)
|
32
|
+
@expressions.any? { |expr| match_expression?(expr, context) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def terminates_on?(context)
|
36
|
+
@terminators.any? { |expr| expr === context }
|
37
|
+
end
|
38
|
+
|
39
|
+
def match_distance(index, context)
|
40
|
+
return 1000 unless expression = @expressions[index]
|
41
|
+
return 0 if expression === context
|
42
|
+
return 1 if Symbol === expression and ActionFlow.flows[expression] === context
|
43
|
+
1000
|
44
|
+
end
|
45
|
+
|
46
|
+
def action_at(index, env, params = {})
|
47
|
+
return nil unless expression = @expressions[index]
|
48
|
+
return ActionFlow.flows[expression].action_at(0, env, params) if Symbol === expression
|
49
|
+
expression.to_params(env).merge(params)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module ActionFlow
|
4
|
+
class Flow
|
5
|
+
|
6
|
+
class Controller
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@context, :params, :session, :request
|
9
|
+
|
10
|
+
def initialize(context)
|
11
|
+
@context = context
|
12
|
+
remove_legacy_objects_from_session!
|
13
|
+
load_states_from_session!
|
14
|
+
end
|
15
|
+
|
16
|
+
def in_flow?(*names)
|
17
|
+
names.any? &status.method(:has_key?)
|
18
|
+
end
|
19
|
+
|
20
|
+
def in_any_flow?
|
21
|
+
not status.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_flow(has_next = false)
|
25
|
+
status.values.
|
26
|
+
sort_by { |state| state.match_distance(self) }.
|
27
|
+
find_all { |state| !has_next or state.next_action }.
|
28
|
+
first
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_session!
|
32
|
+
status.each { |name, state| state.progress!(self) }
|
33
|
+
|
34
|
+
status.each do |name, state|
|
35
|
+
status.delete(name) if state.terminated? or state.complete?
|
36
|
+
end
|
37
|
+
|
38
|
+
new_flow_candidates.each { |name| status[name] = State.new(name) }
|
39
|
+
|
40
|
+
dump_states_to_session!
|
41
|
+
end
|
42
|
+
|
43
|
+
def next_in_flow(*args)
|
44
|
+
flow_name = args.find { |arg| Symbol === arg } || nil
|
45
|
+
params = args.find { |arg| Hash === arg } || {}
|
46
|
+
|
47
|
+
flow_state = status[flow_name] || current_flow(true)
|
48
|
+
flow_state ? flow_state.next_action(params) : nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def status
|
52
|
+
@states
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def remove_legacy_objects_from_session!
|
58
|
+
return unless status = session[:flow_status]
|
59
|
+
flows = ActionFlow.flows
|
60
|
+
status.each do |key, value|
|
61
|
+
status.delete(key) unless Array === value and flows.has_key?(key)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_states_from_session!
|
66
|
+
session_data = session[:flow_status] || {}
|
67
|
+
@states = session_data.inject({}) do |table, (flow_name, data)|
|
68
|
+
table[flow_name] = State.from_session_object(flow_name, data)
|
69
|
+
table
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def dump_states_to_session!
|
74
|
+
session[:flow_status] = @states.inject({}) do |table, (flow_name, state)|
|
75
|
+
table[flow_name] = state.to_session_object
|
76
|
+
table
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def new_flow_candidates
|
81
|
+
return nil unless flows = ActionFlow.flows
|
82
|
+
flows.keys.select do |name|
|
83
|
+
flows[name].begins_with?(self) and
|
84
|
+
not flows[name].mutexes.any?(&method(:in_flow?))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module ActionFlow
|
2
|
+
class Flow
|
3
|
+
|
4
|
+
class State
|
5
|
+
attr_reader :variables
|
6
|
+
|
7
|
+
def initialize(flow_name)
|
8
|
+
@name = flow_name
|
9
|
+
@flow = ActionFlow.flows[flow_name]
|
10
|
+
@index = 0
|
11
|
+
@max_index = 0
|
12
|
+
@variables = {}
|
13
|
+
@complete = false
|
14
|
+
@terminated = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def match_distance(context)
|
18
|
+
@flow.match_distance(@index, context)
|
19
|
+
end
|
20
|
+
|
21
|
+
def progress!(context)
|
22
|
+
if @flow.terminates_on?(context)
|
23
|
+
@terminated = true
|
24
|
+
return
|
25
|
+
end
|
26
|
+
@index += 1 if @flow.match_at?(@index + 1, context)
|
27
|
+
@max_index = [@max_index, @index].max
|
28
|
+
return if current_matches?(context)
|
29
|
+
|
30
|
+
0.upto(@max_index) do |backtrack|
|
31
|
+
@index = backtrack if @flow.match_at?(backtrack, context)
|
32
|
+
end
|
33
|
+
|
34
|
+
@complete = true if @index == @flow.length - 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def complete?
|
38
|
+
@complete
|
39
|
+
end
|
40
|
+
|
41
|
+
def terminated?
|
42
|
+
@terminated
|
43
|
+
end
|
44
|
+
|
45
|
+
def next_action(params = {})
|
46
|
+
@flow.action_at(@index + 1, variables, params)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_session_object
|
50
|
+
[@index, @max_index, @variables, @complete]
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.from_session_object(flow_name, data)
|
54
|
+
instance = new(flow_name)
|
55
|
+
instance.instance_eval do
|
56
|
+
@index = data[0]
|
57
|
+
@max_index = data[1]
|
58
|
+
@variables = data[2]
|
59
|
+
@complete = data[3]
|
60
|
+
end
|
61
|
+
instance
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def current_matches?(context)
|
67
|
+
@flow.match_at?(@index, context)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|