juice10-action_flow 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|