juice10-action_flow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/.document +5 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +20 -0
  4. data/LICENSE.txt +20 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +206 -0
  7. data/Rakefile +53 -0
  8. data/VERSION +1 -0
  9. data/action_flow.gemspec +149 -0
  10. data/init.rb +1 -0
  11. data/install.rb +1 -0
  12. data/lib/action_flow.rb +59 -0
  13. data/lib/action_flow/blank_slate.rb +33 -0
  14. data/lib/action_flow/expression.rb +135 -0
  15. data/lib/action_flow/filters.rb +25 -0
  16. data/lib/action_flow/flow.rb +54 -0
  17. data/lib/action_flow/flow/controller.rb +91 -0
  18. data/lib/action_flow/flow/state.rb +73 -0
  19. data/lib/action_flow/helpers.rb +15 -0
  20. data/lib/action_flow/variable.rb +14 -0
  21. data/test/helper.rb +18 -0
  22. data/test/test_action_flow.rb +7 -0
  23. data/test_app/.gitignore +4 -0
  24. data/test_app/Gemfile +34 -0
  25. data/test_app/Gemfile.lock +131 -0
  26. data/test_app/README +256 -0
  27. data/test_app/Rakefile +7 -0
  28. data/test_app/app/controllers/application_controller.rb +6 -0
  29. data/test_app/app/controllers/settings_controller.rb +33 -0
  30. data/test_app/app/helpers/application_helper.rb +4 -0
  31. data/test_app/app/views/layouts/application.html.erb +14 -0
  32. data/test_app/app/views/settings/_step.html.erb +22 -0
  33. data/test_app/app/views/settings/intro.html.erb +12 -0
  34. data/test_app/app/views/settings/outro.html.erb +8 -0
  35. data/test_app/config.ru +4 -0
  36. data/test_app/config/application.rb +42 -0
  37. data/test_app/config/boot.rb +6 -0
  38. data/test_app/config/cucumber.yml +8 -0
  39. data/test_app/config/database.yml +25 -0
  40. data/test_app/config/environment.rb +5 -0
  41. data/test_app/config/environments/cucumber.rb +35 -0
  42. data/test_app/config/environments/development.rb +26 -0
  43. data/test_app/config/environments/production.rb +49 -0
  44. data/test_app/config/environments/test.rb +35 -0
  45. data/test_app/config/initializers/backtrace_silencers.rb +7 -0
  46. data/test_app/config/initializers/inflections.rb +10 -0
  47. data/test_app/config/initializers/mime_types.rb +5 -0
  48. data/test_app/config/initializers/secret_token.rb +7 -0
  49. data/test_app/config/initializers/session_store.rb +8 -0
  50. data/test_app/config/locales/en.yml +5 -0
  51. data/test_app/config/routes.rb +58 -0
  52. data/test_app/db/schema.rb +15 -0
  53. data/test_app/db/seeds.rb +7 -0
  54. data/test_app/features/flow_management.feature +122 -0
  55. data/test_app/features/flow_with_array_alternatives.feature +13 -0
  56. data/test_app/features/flows_with_conditions.feature +60 -0
  57. data/test_app/features/mutual_exclusion.feature +39 -0
  58. data/test_app/features/step_definitions/flow_steps.rb +20 -0
  59. data/test_app/features/step_definitions/url_steps.rb +7 -0
  60. data/test_app/features/step_definitions/web_steps.rb +211 -0
  61. data/test_app/features/support/env.rb +39 -0
  62. data/test_app/features/support/hooks.rb +3 -0
  63. data/test_app/features/support/paths.rb +33 -0
  64. data/test_app/features/support/selectors.rb +39 -0
  65. data/test_app/lib/tasks/.gitkeep +0 -0
  66. data/test_app/lib/tasks/cucumber.rake +57 -0
  67. data/test_app/public/404.html +26 -0
  68. data/test_app/public/422.html +26 -0
  69. data/test_app/public/500.html +26 -0
  70. data/test_app/public/favicon.ico +0 -0
  71. data/test_app/public/images/rails.png +0 -0
  72. data/test_app/public/index.html +239 -0
  73. data/test_app/public/javascripts/application.js +2 -0
  74. data/test_app/public/javascripts/controls.js +965 -0
  75. data/test_app/public/javascripts/dragdrop.js +974 -0
  76. data/test_app/public/javascripts/effects.js +1123 -0
  77. data/test_app/public/javascripts/prototype.js +6001 -0
  78. data/test_app/public/javascripts/rails.js +191 -0
  79. data/test_app/public/robots.txt +5 -0
  80. data/test_app/public/stylesheets/.gitkeep +0 -0
  81. data/test_app/script/about +4 -0
  82. data/test_app/script/console +3 -0
  83. data/test_app/script/cucumber +10 -0
  84. data/test_app/script/dbconsole +3 -0
  85. data/test_app/script/destroy +3 -0
  86. data/test_app/script/generate +3 -0
  87. data/test_app/script/performance/benchmarker +3 -0
  88. data/test_app/script/performance/profiler +3 -0
  89. data/test_app/script/plugin +3 -0
  90. data/test_app/script/rails +6 -0
  91. data/test_app/script/runner +3 -0
  92. data/test_app/script/server +3 -0
  93. data/test_app/test/performance/browsing_test.rb +9 -0
  94. data/test_app/test/test_helper.rb +13 -0
  95. data/test_app/vendor/plugins/.gitkeep +0 -0
  96. data/test_app/vendor/plugins/action_flow/init.rb +2 -0
  97. data/uninstall.rb +1 -0
  98. metadata +224 -0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/lib/action_flow'
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -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
+