hero 0.0.3 → 0.0.4

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/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
-