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.
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
+