hero 0.0.1 → 0.0.2
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 +174 -0
- data/lib/hero/formula.rb +52 -0
- data/lib/{hero_observer.rb → hero/observer.rb} +0 -0
- data/lib/hero.rb +2 -51
- data/spec/hero_spec.rb +6 -0
- metadata +5 -3
data/README.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# Hero
|
2
|
+
|
3
|
+
## Its a bird, its a plane, its... its... my Hero
|
4
|
+
|
5
|
+
Ever wish that you could unwind the spaghetti and get out of the corner you've been backed into?
|
6
|
+
|
7
|
+
### Hero is here to save the day
|
8
|
+
|
9
|
+
I've seen my share of poor app structure.
|
10
|
+
Hell, I wrote most of it.
|
11
|
+
Whether is fat controllers, giant models with mystery callbacks, or a junk drawer lib directory.
|
12
|
+
|
13
|
+
The question remains. **Where do I put my business logic?**
|
14
|
+
|
15
|
+
Finally... an answer that might even make DHH proud.
|
16
|
+
One that evolved from the real world with concrete use cases and actual production code.
|
17
|
+
|
18
|
+
## Process Modeling
|
19
|
+
|
20
|
+
The problem has always been: How to effectively model a business process within your app.
|
21
|
+
|
22
|
+
Things start simply enough but eventually edge cases force *gothcas* into
|
23
|
+
various libs, modules, and classes. Before you know you it,
|
24
|
+
you have a lump of spaghetti that's difficult to maintain and even harder to improve.
|
25
|
+
|
26
|
+
### Enter Hero
|
27
|
+
|
28
|
+
Hero provides a simple pattern that encourages you to
|
29
|
+
<a href="http://en.wikipedia.org/wiki/Decomposition_(computer_science)">decompose</a>
|
30
|
+
business processes into managable chunks.
|
31
|
+
|
32
|
+
And... the best part is, they are easily tested.
|
33
|
+
|
34
|
+
---
|
35
|
+
|
36
|
+
Here's an example.
|
37
|
+
Assume we have a Rails app that needs to support logins.
|
38
|
+
|
39
|
+
Our implementation might look something like this.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# app/controllers/logins_controller.rb
|
43
|
+
class LoginsController < ApplicationController
|
44
|
+
|
45
|
+
def create
|
46
|
+
if user = User.authenticate(params[:username], params[:password])
|
47
|
+
session[:current_user_id] = user.id
|
48
|
+
redirect_to root_url
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def destroy
|
53
|
+
@_current_user = session[:current_user_id] = nil
|
54
|
+
redirect_to root_url
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# app/controllers/application_controller.rb
|
62
|
+
class ApplicationController < ActionController::Base
|
63
|
+
before_filter :require_login
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def require_login
|
68
|
+
unless logged_in?
|
69
|
+
flash[:error] = "You must be logged in to access this section"
|
70
|
+
redirect_to new_login_url # halts request cycle
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def logged_in?
|
75
|
+
!!current_user
|
76
|
+
end
|
77
|
+
|
78
|
+
def current_user
|
79
|
+
@_current_user ||= session[:current_user_id] &&
|
80
|
+
User.find_by_id(session[:current_user_id])
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
Hero approaches this problem differently.
|
87
|
+
It asks us to <a href="http://en.wikipedia.org/wiki/Decomposition_(computer_science)">decompose</a>
|
88
|
+
the login requirement into business processes which might look something like this.
|
89
|
+
|
90
|
+
#### Login
|
91
|
+
|
92
|
+
1. Authenticate the user
|
93
|
+
1. Save user info to session
|
94
|
+
1. Send user to home page
|
95
|
+
|
96
|
+
#### Logout
|
97
|
+
|
98
|
+
1. Remove user session
|
99
|
+
1. Send user to home page
|
100
|
+
|
101
|
+
#### Protect Page
|
102
|
+
|
103
|
+
1. Verify the user is logged in
|
104
|
+
|
105
|
+
Note that we just defined an [ontology](http://en.wikipedia.org/wiki/Process_ontology)
|
106
|
+
that can be used to discuss the requirement and its implementation with non developers.
|
107
|
+
|
108
|
+
Here's an example of an implementation with Hero.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# lib/errors.rb
|
112
|
+
class AuthenticationError < StandardError; end
|
113
|
+
class AuthorizationError < StandardError; end
|
114
|
+
```
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
# config/initializers/login.rb
|
118
|
+
Hero::Formula[:login].add_step do |context|
|
119
|
+
user = User.authenticate(context.params[:username], context.params[:password])
|
120
|
+
raise AuthenticationError unless user
|
121
|
+
context.session[:current_user_id] = user.id
|
122
|
+
end
|
123
|
+
|
124
|
+
Hero::Formula[:logout].add_step do |context|
|
125
|
+
context.session[:current_user_id] = nil
|
126
|
+
end
|
127
|
+
|
128
|
+
Hero::Formula[:protect_page].add_step do |context|
|
129
|
+
raise AuthorizationError if context.session[:current_user_id].nil?
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
# app/controllers/logins_controller.rb
|
135
|
+
class LoginsController < ApplicationController
|
136
|
+
rescue_from AuthenticationError { render "new" }
|
137
|
+
|
138
|
+
def create
|
139
|
+
Hero::Formula[:login].run(self)
|
140
|
+
redirect_to root_url
|
141
|
+
end
|
142
|
+
|
143
|
+
def destroy
|
144
|
+
Hero::Formula[:logout].run(self)
|
145
|
+
redirect_to root_url
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# app/controllers/application_controller.rb
|
153
|
+
class ApplicationController < ActionController::Base
|
154
|
+
before_filter :protect
|
155
|
+
rescue_from AuthorizationError, :with => :force_login
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def protect
|
160
|
+
Hero::Formula[:protect_page].run(self)
|
161
|
+
end
|
162
|
+
|
163
|
+
def force_login
|
164
|
+
flash[:error] = "You must be logged in to access this section"
|
165
|
+
redirect_to new_login_url
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
I know what you're thinking, and you're right.
|
172
|
+
This doesn't pass DHH's before/after test,
|
173
|
+
but lets start throwing edge cases at it and see what happens.
|
174
|
+
|
data/lib/hero/formula.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'observer'
|
2
|
+
require 'singleton'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Hero
|
6
|
+
class Formula
|
7
|
+
include Observable
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
class << self
|
11
|
+
extend Forwardable
|
12
|
+
def_delegator :formulas, :each, :each
|
13
|
+
def_delegator :formulas, :length, :count
|
14
|
+
|
15
|
+
def reset
|
16
|
+
@formulas = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](name)
|
20
|
+
formulas[name] ||= register(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def register(name)
|
24
|
+
observer = Hero::Observer.new
|
25
|
+
formula = Class.new(Hero::Formula).instance
|
26
|
+
formula.add_observer(observer)
|
27
|
+
formula.instance_eval { @observer = observer }
|
28
|
+
formulas[name] = formula
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def formulas
|
34
|
+
@formulas ||= {}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_step(step=nil, &block)
|
39
|
+
step ||= block if block_given?
|
40
|
+
@observer.steps << step
|
41
|
+
end
|
42
|
+
|
43
|
+
def notify(target=nil, options={})
|
44
|
+
changed
|
45
|
+
notify_observers(target, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
alias :run :notify
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
File without changes
|
data/lib/hero.rb
CHANGED
@@ -1,52 +1,3 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require 'forwardable'
|
4
|
-
|
5
|
-
module Hero
|
6
|
-
class Formula
|
7
|
-
include Observable
|
8
|
-
include Singleton
|
9
|
-
|
10
|
-
class << self
|
11
|
-
extend Forwardable
|
12
|
-
def_delegator :formulas, :each, :each
|
13
|
-
def_delegator :formulas, :length, :count
|
14
|
-
|
15
|
-
def reset
|
16
|
-
@formulas = {}
|
17
|
-
end
|
18
|
-
|
19
|
-
def [](name)
|
20
|
-
formulas[name]
|
21
|
-
end
|
22
|
-
|
23
|
-
def register(name)
|
24
|
-
observer = Hero::Observer.new
|
25
|
-
formula = Class.new(Hero::Formula).instance
|
26
|
-
formula.add_observer(observer)
|
27
|
-
formula.instance_eval { @observer = observer }
|
28
|
-
formulas[name] = formula
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def formulas
|
34
|
-
@formulas ||= {}
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def add_step(step=nil, &block)
|
39
|
-
step ||= block if block_given?
|
40
|
-
@observer.steps << step
|
41
|
-
end
|
42
|
-
|
43
|
-
def notify(target=nil, options={})
|
44
|
-
changed
|
45
|
-
notify_observers(target, options)
|
46
|
-
end
|
47
|
-
|
48
|
-
alias :run :notify
|
49
|
-
|
50
|
-
end
|
1
|
+
Dir[File.join(File.dirname(__FILE__), "hero", "*rb")].each do |file|
|
2
|
+
require file
|
51
3
|
end
|
52
|
-
|
data/spec/hero_spec.rb
CHANGED
@@ -24,6 +24,12 @@ describe Hero::Formula do
|
|
24
24
|
assert Hero::Formula[:test_formula].is_a? Hero::Formula
|
25
25
|
end
|
26
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
|
+
|
27
33
|
it "should support registering N number of formulas" do
|
28
34
|
10.times { |i| Hero::Formula.register("example_#{i}") }
|
29
35
|
assert_equal Hero::Formula.count, 10
|
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.
|
4
|
+
version: 0.0.2
|
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-
|
12
|
+
date: 2012-08-23 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: ! ' Simplify your apps with Hero.
|
15
15
|
|
@@ -20,10 +20,12 @@ executables: []
|
|
20
20
|
extensions: []
|
21
21
|
extra_rdoc_files: []
|
22
22
|
files:
|
23
|
+
- lib/hero/formula.rb
|
24
|
+
- lib/hero/observer.rb
|
23
25
|
- lib/hero.rb
|
24
|
-
- lib/hero_observer.rb
|
25
26
|
- Gemfile
|
26
27
|
- Gemfile.lock
|
28
|
+
- README.md
|
27
29
|
- spec/hero_spec.rb
|
28
30
|
homepage: http://hopsoft.github.com/hero/
|
29
31
|
licenses:
|