hero 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # Hero
2
2
 
3
+ ![Hero GEM](http://hopsoft.github.com/hero/images/hero.jpg)
4
+
3
5
  ## Its a bird, its a plane, its... its... my Hero
4
6
 
5
- ![Hero GEM](http://hopsoft.github.com/hero/images/hero.jpg)
7
+ ---
6
8
 
7
- Ever wish that you could unwind the spaghetti and get out of the corner you've been backed into?
9
+ *Controlling complexity is the essence of computer programming. -- [Brian Kernighan](http://en.wikipedia.org/wiki/Brian_Kernighan)*
8
10
 
9
- ### Hero is here to save the day
11
+ ---
10
12
 
11
13
  I've seen my share of poor app structure.
12
14
  Hell, I wrote most of it.
@@ -17,161 +19,25 @@ The question remains. **Where do I put my business logic?**
17
19
  Finally... an answer that might even make DHH proud.
18
20
  One that evolved from the real world with concrete use cases and actual production code.
19
21
 
20
- ## Process Modeling
22
+ ## Why Hero?
21
23
 
22
- The problem has always been: How to effectively model a business process within your app.
24
+ * App structure matches the mental map of your business
25
+ * Testable coponents
26
+ * Faster ramp up time for new team members
27
+ * Easily handles changing requirements
28
+
29
+ ### Process Modeling
30
+
31
+ The problem has always been: **How do you effectively model a business process within your app?**
23
32
 
24
33
  Things start simply enough but eventually edge cases force *gotchas* into
25
34
  various libs, modules, and classes. Before you know you it,
26
35
  you have a lump of spaghetti that's difficult to maintain and even harder to improve.
27
36
 
28
- ### Enter Hero
29
-
30
37
  Hero provides a simple pattern that encourages you to
31
38
  <a href="http://en.wikipedia.org/wiki/Decomposition_(computer_science)">decompose</a>
32
- business processes into managable chunks.
33
-
34
- And... the best part is, they are easily tested.
39
+ these processes into managable chunks. And the best part... the components can be easily tested.
35
40
 
36
41
  ---
37
42
 
38
- Here's an example.
39
- Assume we have a Rails app that needs to support logins.
40
-
41
- Our implementation might look something like this.
42
-
43
- ```ruby
44
- # app/controllers/logins_controller.rb
45
- class LoginsController < ApplicationController
46
-
47
- def create
48
- if user = User.authenticate(params[:username], params[:password])
49
- session[:current_user_id] = user.id
50
- redirect_to root_url
51
- end
52
- end
53
-
54
- def destroy
55
- @_current_user = session[:current_user_id] = nil
56
- redirect_to root_url
57
- end
58
-
59
- end
60
- ```
61
-
62
- ```ruby
63
- # app/controllers/application_controller.rb
64
- class ApplicationController < ActionController::Base
65
- before_filter :require_login
66
-
67
- private
68
-
69
- def require_login
70
- unless logged_in?
71
- flash[:error] = "You must be logged in to access this section"
72
- redirect_to new_login_url # halts request cycle
73
- end
74
- end
75
-
76
- def logged_in?
77
- !!current_user
78
- end
79
-
80
- def current_user
81
- @_current_user ||= session[:current_user_id] &&
82
- User.find_by_id(session[:current_user_id])
83
- end
84
-
85
- end
86
- ```
87
-
88
- Hero approaches this problem differently.
89
- It asks us to <a href="http://en.wikipedia.org/wiki/Decomposition_(computer_science)">decompose</a>
90
- the login requirement into a business processes looks something like this.
91
-
92
- #### Login
93
-
94
- 1. Authenticate the user
95
- 1. Save user info to session
96
- 1. Send user to home page
97
-
98
- #### Logout
99
-
100
- 1. Remove user session
101
- 1. Send user to home page
102
-
103
- #### Protect Page
104
-
105
- 1. Verify the user is logged in
106
-
107
- Note that we just defined an [ontology](http://en.wikipedia.org/wiki/Process_ontology)
108
- that can be used to discuss the requirement and its implementation with non developers.
109
-
110
- Here's an example of an implementation with Hero.
111
-
112
- ```ruby
113
- # lib/errors.rb
114
- class AuthenticationError < StandardError; end
115
- class AuthorizationError < StandardError; end
116
- ```
117
-
118
- ```ruby
119
- # config/initializers/login.rb
120
- Hero::Formula[:login].add_step do |context|
121
- user = User.authenticate(context.params[:username], context.params[:password])
122
- raise AuthenticationError unless user
123
- context.session[:current_user_id] = user.id
124
- end
125
-
126
- Hero::Formula[:logout].add_step do |context|
127
- context.session[:current_user_id] = nil
128
- end
129
-
130
- Hero::Formula[:protect_page].add_step do |context|
131
- raise AuthorizationError if context.session[:current_user_id].nil?
132
- end
133
- ```
134
-
135
- ```ruby
136
- # app/controllers/logins_controller.rb
137
- class LoginsController < ApplicationController
138
- rescue_from AuthenticationError { render "new" }
139
-
140
- def create
141
- Hero::Formula[:login].run(self)
142
- redirect_to root_url
143
- end
144
-
145
- def destroy
146
- Hero::Formula[:logout].run(self)
147
- redirect_to root_url
148
- end
149
-
150
- end
151
- ```
152
-
153
- ```ruby
154
- # app/controllers/application_controller.rb
155
- class ApplicationController < ActionController::Base
156
- before_filter :protect
157
- rescue_from AuthorizationError, :with => :force_login
158
-
159
- private
160
-
161
- def protect
162
- Hero::Formula[:protect_page].run(self)
163
- end
164
-
165
- def force_login
166
- flash[:error] = "You must be logged in to access this section"
167
- redirect_to new_login_url
168
- end
169
-
170
- end
171
- ```
172
-
173
- I know what you're thinking, and you're right.
174
- This doesn't pass DHH's before/after test,
175
- but lets start throwing edge cases at it and see what happens.
176
-
177
43
  More soon. Stay tuned...
data/lib/hero.rb CHANGED
@@ -1,3 +1,9 @@
1
1
  Dir[File.join(File.dirname(__FILE__), "hero", "*rb")].each do |file|
2
2
  require file
3
3
  end
4
+
5
+ module Hero
6
+ class << self
7
+ attr_accessor :logger
8
+ end
9
+ end
data/lib/hero/formula.rb CHANGED
@@ -1,17 +1,26 @@
1
1
  require 'observer'
2
2
  require 'singleton'
3
3
  require 'forwardable'
4
+ require File.join(File.dirname(__FILE__), "observer")
4
5
 
5
6
  module Hero
7
+
6
8
  class Formula
7
- include Observable
8
- include Singleton
9
9
 
10
+ # Class attributes & methods ==============================================
10
11
  class << self
11
12
  extend Forwardable
12
13
  def_delegator :formulas, :each, :each
13
14
  def_delegator :formulas, :length, :count
14
15
 
16
+ def publish
17
+ value = []
18
+ each do |name, formula|
19
+ value << formula.publish
20
+ end
21
+ value.join("\n\n")
22
+ end
23
+
15
24
  def reset
16
25
  formulas.values.each { |f| f.delete_observers }
17
26
  @formulas = {}
@@ -22,10 +31,13 @@ module Hero
22
31
  end
23
32
 
24
33
  def register(name)
25
- observer = Hero::Observer.new
34
+ observer = Hero::Observer.new(name)
26
35
  formula = Class.new(Hero::Formula).instance
27
36
  formula.add_observer(observer)
28
- formula.instance_eval { @observer = observer }
37
+ formula.instance_eval do
38
+ @name = name
39
+ @observer = observer
40
+ end
29
41
  formulas[name] = formula
30
42
  end
31
43
 
@@ -36,9 +48,21 @@ module Hero
36
48
  end
37
49
  end
38
50
 
39
- def add_step(step=nil, &block)
40
- step ||= block if block_given?
41
- @observer.steps << step
51
+ # Instance attributes & methods ===========================================
52
+ extend Forwardable
53
+ include Observable
54
+ include Singleton
55
+
56
+ attr_reader :name, :observer
57
+ def_delegator :observer, :steps, :steps
58
+ def_delegator :observer, :add_step, :add_step
59
+
60
+ def publish
61
+ value = [name]
62
+ steps.each_with_index do |step, index|
63
+ value << "#{(index + 1).to_s.rjust(3)}. #{step.keys.first}"
64
+ end
65
+ value.join("\n")
42
66
  end
43
67
 
44
68
  def notify(context=nil, options={})
data/lib/hero/observer.rb CHANGED
@@ -1,13 +1,31 @@
1
1
  module Hero
2
2
  class Observer
3
+ attr_reader :formula_name
4
+
5
+ def initialize(formula_name)
6
+ @formula_name = formula_name
7
+ end
3
8
 
4
9
  def steps
5
- @steps ||= []
10
+ @steps ||= {}
11
+ @steps.sort{ |a, b| a.last[:index] <=> b.last[:index] }.map{ |k, v| { k => v[:step] } }
12
+ end
13
+
14
+ def add_step(name, step=nil, &block)
15
+ @steps ||= {}
16
+ step ||= block if block_given?
17
+ @steps[name] = { :step => step, :index => @steps.length }
6
18
  end
7
19
 
8
20
  def update(context, options={})
9
- steps.each { |step| step.call(context, options) }
21
+ steps.each do |step|
22
+ if Hero.logger
23
+ Hero.logger.info "HERO Formula: #{formula_name}, Step: #{step.keys.first}, Context: #{context.inspect}, Options: #{options.inspect}"
24
+ end
25
+ step.values.first.call(context, options)
26
+ end
10
27
  end
11
28
 
12
29
  end
13
30
  end
31
+
@@ -0,0 +1,138 @@
1
+ require "spec_helper"
2
+
3
+ describe Hero::Formula do
4
+ include GrumpyOldMan
5
+
6
+ before :each do
7
+ Hero.logger = nil
8
+ Hero::Formula.reset
9
+ end
10
+
11
+ it "should support reset" do
12
+ Hero::Formula.register(:test_formula)
13
+ assert_equal Hero::Formula.count, 1
14
+ Hero::Formula.reset
15
+ assert_equal Hero::Formula.count, 0
16
+ end
17
+
18
+ it "should support registering a formula" do
19
+ Hero::Formula.register(:test_formula)
20
+ assert_equal Hero::Formula.count, 1
21
+ assert Hero::Formula[:test_formula].is_a? Hero::Formula
22
+ end
23
+
24
+ it "should auto register formulas" do
25
+ Hero::Formula[:test_formula]
26
+ assert_equal Hero::Formula.count, 1
27
+ assert Hero::Formula[:test_formula].is_a? Hero::Formula
28
+ end
29
+
30
+ it "should support registering N number of formulas" do
31
+ 10.times { |i| Hero::Formula.register("example_#{i}") }
32
+ assert_equal Hero::Formula.count, 10
33
+ end
34
+
35
+ it "should unregister formula observers on reset" do
36
+ formula = Hero::Formula[:test_formula]
37
+ assert_equal Hero::Formula.count, 1
38
+ formula.add_step(:one) {}
39
+ assert_equal formula.count_observers, 1
40
+ Hero::Formula.reset
41
+ assert_equal Hero::Formula.count, 0
42
+ assert_equal formula.count_observers, 0
43
+ end
44
+
45
+ it "should publish all formulas" do
46
+ Hero::Formula[:first].add_step(:one) {}
47
+ Hero::Formula[:first].add_step(:two) {}
48
+ Hero::Formula[:first].add_step(:three) {}
49
+ Hero::Formula[:first].add_step(:four) {}
50
+
51
+ Hero::Formula[:second].add_step(:one) {}
52
+ Hero::Formula[:second].add_step(:two) {}
53
+ Hero::Formula[:second].add_step(:three) {}
54
+ Hero::Formula[:second].add_step(:four) {}
55
+
56
+ expected = "first 1. one 2. two 3. three 4. foursecond 1. one 2. two 3. three 4. four"
57
+ assert_equal Hero::Formula.publish.gsub(/\n/, ""), expected
58
+ end
59
+
60
+ describe "a registered formula" do
61
+ it "should support adding steps" do
62
+ Hero::Formula[:test_formula].add_step(:one) { }
63
+ assert_equal Hero::Formula.count, 1
64
+ assert_equal Hero::Formula[:test_formula].steps.length, 1
65
+ end
66
+
67
+ def invoke_notify_method(name)
68
+ step_ran = false
69
+ target = Object.new
70
+ Hero::Formula[:test_formula].add_step(:one) do |t, opts|
71
+ assert_equal t, target
72
+ assert_equal opts[:foo], :bar
73
+ step_ran = true
74
+ end
75
+ Hero::Formula[:test_formula].notify(target, :foo => :bar)
76
+ assert step_ran
77
+ end
78
+
79
+ it "should support notify" do
80
+ invoke_notify_method(:notify)
81
+ end
82
+
83
+ it "should support run" do
84
+ invoke_notify_method(:run)
85
+ end
86
+
87
+ it "should support running step defined in a class" do
88
+ class Step
89
+ def call(context, options)
90
+ options[:context] = context
91
+ end
92
+ end
93
+
94
+ opts = {}
95
+ Hero::Formula[:test_formula].add_step(:one, Step.new)
96
+ Hero::Formula[:test_formula].run(:foo, opts)
97
+ assert_equal opts[:context], :foo
98
+ end
99
+
100
+ it "should support running multiple tests" do
101
+ log = {}
102
+ Hero::Formula[:test_formula].add_step(:one) { |o, l| l[:one] = true }
103
+ Hero::Formula[:test_formula].add_step(:two) { |o, l| l[:two] = true }
104
+ Hero::Formula[:test_formula].run(self, log)
105
+ assert log[:one]
106
+ assert log[:two]
107
+ end
108
+
109
+ it "should publish all steps in the formula" do
110
+ Hero::Formula[:test_formula].add_step(:one) {}
111
+ Hero::Formula[:test_formula].add_step(:two) {}
112
+ Hero::Formula[:test_formula].add_step(:three) {}
113
+ Hero::Formula[:test_formula].add_step(:four) {}
114
+ expected = "test_formula 1. one 2. two 3. three 4. four"
115
+ assert_equal Hero::Formula[:test_formula].publish.gsub(/\n/, ""), expected
116
+ end
117
+
118
+ it "should support logging" do
119
+ class TestLogger
120
+ attr_reader :buffer
121
+ def info(value)
122
+ @buffer ||= []
123
+ @buffer << value
124
+ end
125
+ end
126
+ Hero.logger = TestLogger.new
127
+ Hero::Formula[:test_formula].add_step(:one) {}
128
+ Hero::Formula[:test_formula].add_step(:two) {}
129
+ Hero::Formula[:test_formula].run(:example, :logging => true)
130
+ assert_equal Hero.logger.buffer.length, 2
131
+ assert_equal "HERO Formula: test_formula, Step: one, Context: :example, Options: {:logging=>true}", Hero.logger.buffer.first
132
+ assert_equal "HERO Formula: test_formula, Step: two, Context: :example, Options: {:logging=>true}", Hero.logger.buffer.last
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
@@ -0,0 +1,39 @@
1
+ require "spec_helper"
2
+
3
+ describe "Hero::Observer instance" do
4
+ include GrumpyOldMan
5
+
6
+ it "should support add_step" do
7
+ step = lambda {}
8
+ o = Hero::Observer.new(:example)
9
+ o.add_step(:one, step)
10
+ assert_equal o.steps.length, 1
11
+ assert_equal o.steps[0].keys.first, :one
12
+ assert_equal o.steps[0].values.first, step
13
+ end
14
+
15
+ it "should support properly handle a double add" do
16
+ step1 = lambda {}
17
+ step2 = lambda {}
18
+ o = Hero::Observer.new(:example)
19
+ o.add_step(:one, step1)
20
+ o.add_step(:one, step2)
21
+ assert_equal o.steps.length, 1
22
+ assert_equal o.steps[0].keys.first, :one
23
+ assert_equal o.steps[0].values.first, step2
24
+ end
25
+
26
+ it "should properly sort steps based on the order they were added" do
27
+ o = Hero::Observer.new(:example)
28
+ o.add_step(:one) {}
29
+ o.add_step(:two) {}
30
+ o.add_step(:three) {}
31
+ o.add_step(:four) {}
32
+ o.add_step(:one) {}
33
+ assert_equal o.steps.length, 4
34
+ assert_equal o.steps[0].keys.first, :two
35
+ assert_equal o.steps[1].keys.first, :three
36
+ assert_equal o.steps[2].keys.first, :four
37
+ assert_equal o.steps[3].keys.first, :one
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ require "pry"
2
+ require "grumpy_old_man"
3
+ Dir[File.join(File.dirname(__FILE__), "..", "lib", "*.rb")].each do |file|
4
+ require file
5
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hero
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-23 00:00:00.000000000 Z
12
+ date: 2012-08-24 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: ! ' Simplify your apps with Hero.
15
15
 
@@ -26,7 +26,9 @@ files:
26
26
  - Gemfile
27
27
  - Gemfile.lock
28
28
  - README.md
29
- - spec/hero_spec.rb
29
+ - spec/formula_spec.rb
30
+ - spec/observer_spec.rb
31
+ - spec/spec_helper.rb
30
32
  homepage: http://hopsoft.github.com/hero/
31
33
  licenses:
32
34
  - MIT
data/spec/hero_spec.rb DELETED
@@ -1,70 +0,0 @@
1
- require "pry"
2
- require "grumpy_old_man"
3
- Dir[File.join(File.dirname(__FILE__), "..", "lib", "*.rb")].each do |file|
4
- require file
5
- end
6
-
7
- describe Hero::Formula do
8
- include GrumpyOldMan
9
-
10
- before :each do
11
- Hero::Formula.reset
12
- end
13
-
14
- it "should support reset" do
15
- Hero::Formula.register(:test_formula)
16
- assert_equal Hero::Formula.count, 1
17
- Hero::Formula.reset
18
- assert_equal Hero::Formula.count, 0
19
- end
20
-
21
- it "should support registering a formula" do
22
- Hero::Formula.register(:test_formula)
23
- assert_equal Hero::Formula.count, 1
24
- assert Hero::Formula[:test_formula].is_a? Hero::Formula
25
- end
26
-
27
- it "should auto register formulas" do
28
- Hero::Formula[:test_formula]
29
- assert_equal Hero::Formula.count, 1
30
- assert Hero::Formula[:test_formula].is_a? Hero::Formula
31
- end
32
-
33
- it "should support registering N number of formulas" do
34
- 10.times { |i| Hero::Formula.register("example_#{i}") }
35
- assert_equal Hero::Formula.count, 10
36
- end
37
-
38
- it "should unregister formula observers on reset" do
39
- formula = Hero::Formula[:test_formula]
40
- assert_equal Hero::Formula.count, 1
41
- formula.add_step {}
42
- assert_equal formula.count_observers, 1
43
- Hero::Formula.reset
44
- assert_equal Hero::Formula.count, 0
45
- assert_equal formula.count_observers, 0
46
- end
47
-
48
- describe "a registered formula" do
49
- it "should support adding steps" do
50
- Hero::Formula.register(:test_formula)
51
- Hero::Formula[:test_formula].add_step { }
52
- end
53
-
54
- it "should support notify" do
55
- Hero::Formula.register(:test_formula)
56
- step_ran = false
57
- target = Object.new
58
- Hero::Formula[:test_formula].add_step do |t, opts|
59
- assert_equal t, target
60
- assert_equal opts[:foo], :bar
61
- step_ran = true
62
- end
63
- Hero::Formula[:test_formula].notify(target, :foo => :bar)
64
- assert step_ran
65
- end
66
-
67
- end
68
-
69
- end
70
-