hero 0.0.4 → 0.0.5
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 +143 -6
- data/lib/hero.rb +7 -0
- data/lib/hero/formula.rb +1 -1
- data/lib/hero/observer.rb +49 -6
- data/spec/observer_spec.rb +8 -8
- metadata +2 -2
data/README.md
CHANGED
@@ -21,12 +21,12 @@ One that evolved from the real world with concrete use cases and actual producti
|
|
21
21
|
|
22
22
|
## Why Hero?
|
23
23
|
|
24
|
-
*
|
25
|
-
*
|
26
|
-
*
|
27
|
-
*
|
24
|
+
* It matches the mental map of your business requirements
|
25
|
+
* It produces testable components
|
26
|
+
* It easily handles changing requirements
|
27
|
+
* It reduces the ramp up time for new team members
|
28
28
|
|
29
|
-
|
29
|
+
## Process Modeling
|
30
30
|
|
31
31
|
The problem has always been: **How do you effectively model a business process within your app?**
|
32
32
|
|
@@ -40,4 +40,141 @@ these processes into managable chunks. And the best part... the components can b
|
|
40
40
|
|
41
41
|
---
|
42
42
|
|
43
|
-
|
43
|
+
## Quick Start
|
44
|
+
|
45
|
+
```bash
|
46
|
+
gem install hero
|
47
|
+
```
|
48
|
+
|
49
|
+
Lets model a business process for collecting the top news stories from Hacker News, Reddit, & Google and then emailing the results to someone.
|
50
|
+
|
51
|
+
Gather News
|
52
|
+
|
53
|
+
- Get news from Hacker News
|
54
|
+
- Get news from Reddit
|
55
|
+
- Get news from Google
|
56
|
+
- Email Results
|
57
|
+
|
58
|
+
Now that we have the basic requirements, lets model it with Hero.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
Hero::Formula[:gather_news].add_step :hacker_news do |context|
|
62
|
+
# make api call
|
63
|
+
# parse results
|
64
|
+
# append results to context
|
65
|
+
end
|
66
|
+
|
67
|
+
Hero::Formula[:gather_news].add_step :reddit do |context|
|
68
|
+
# make api call
|
69
|
+
# parse results
|
70
|
+
# append results to context
|
71
|
+
end
|
72
|
+
|
73
|
+
Hero::Formula[:gather_news].add_step :google do |context|
|
74
|
+
# make api call
|
75
|
+
# parse results
|
76
|
+
# append results to context
|
77
|
+
end
|
78
|
+
|
79
|
+
Hero::Formula[:gather_news].add_step :email do |context|
|
80
|
+
# format news for email
|
81
|
+
# compose the email
|
82
|
+
# send the email
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
This looks surprising similar to the requirements.
|
87
|
+
In fact we can publish the specification directly from Hero.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
puts Hero::Formula[:gather_news].publish
|
91
|
+
|
92
|
+
# => gather_news
|
93
|
+
# 1. hacker_news
|
94
|
+
# 2. reddit
|
95
|
+
# 3. google
|
96
|
+
# 4. email
|
97
|
+
```
|
98
|
+
|
99
|
+
Pretty slick.
|
100
|
+
Now... lets run the process.
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
news = {}
|
104
|
+
Hero::Formula[:gather_news].run(news)
|
105
|
+
```
|
106
|
+
|
107
|
+
And we're done.
|
108
|
+
|
109
|
+
### Key take aways
|
110
|
+
|
111
|
+
- **The implementation aligns perfectly with the requirements.**
|
112
|
+
*This means that developers and business folks can talk the same lingo.*
|
113
|
+
|
114
|
+
- **The formula is composed of smaller steps that are interchangable.**
|
115
|
+
*This means we are poised for changing requirements.*
|
116
|
+
|
117
|
+
- **Each step implements the interface `def call(context)`**
|
118
|
+
*This means we can create step classes to simplify the app structure.*
|
119
|
+
|
120
|
+
## Next Steps
|
121
|
+
|
122
|
+
As our app grows in complexity, we should change the steps from blocks to classes.
|
123
|
+
Here's an example.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
# this
|
127
|
+
Hero::Formula[:gather_news].add_step :hacker_news do |context|
|
128
|
+
# make api call
|
129
|
+
# parse results
|
130
|
+
# append results to context
|
131
|
+
end
|
132
|
+
|
133
|
+
# changes to this
|
134
|
+
module GatherNews
|
135
|
+
class HackerNews
|
136
|
+
|
137
|
+
def call(context)
|
138
|
+
# make api call
|
139
|
+
# parse results
|
140
|
+
# append results to context
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
Hero::Formula[:gather_news].add_step GatherNews::HackerNews.new
|
147
|
+
```
|
148
|
+
|
149
|
+
We should also create a directory structure that maps to the business process.
|
150
|
+
Something like this.
|
151
|
+
|
152
|
+
```bash
|
153
|
+
|-app
|
154
|
+
|-formulas
|
155
|
+
|-gather_news
|
156
|
+
|-hacker_news.rb
|
157
|
+
|-reddit.rb
|
158
|
+
|-google.rb
|
159
|
+
|-email.rb
|
160
|
+
```
|
161
|
+
|
162
|
+
We also need an initializer to set the formula up.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
# app/initializer.rb
|
166
|
+
Hero::Formula[:gather_news].add_step GatherNews::HackerNews.new
|
167
|
+
Hero::Formula[:gather_news].add_step GatherNews::Reddit.new
|
168
|
+
Hero::Formula[:gather_news].add_step GatherNews::Google.new
|
169
|
+
Hero::Formula[:gather_news].add_step GatherNews::Email.new
|
170
|
+
```
|
171
|
+
|
172
|
+
Now we have a well structured application that is ready to grow.
|
173
|
+
Notice how well organized everything is.
|
174
|
+
|
175
|
+
Also note that we can write tests for each step independent of anything else.
|
176
|
+
This is an important point and a powerful concept.
|
177
|
+
|
178
|
+
## Deep Cuts
|
179
|
+
|
180
|
+
Advanced usage coming soon...
|
data/lib/hero.rb
CHANGED
@@ -4,6 +4,13 @@ end
|
|
4
4
|
|
5
5
|
module Hero
|
6
6
|
class << self
|
7
|
+
|
8
|
+
# You can optionally register a Logger with Hero.
|
9
|
+
# If set, a logger.info message will be written for each step called when running Hero::Formulas.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# Hero.logger = Logger.new(STDOUT)
|
7
13
|
attr_accessor :logger
|
14
|
+
|
8
15
|
end
|
9
16
|
end
|
data/lib/hero/formula.rb
CHANGED
data/lib/hero/observer.rb
CHANGED
@@ -1,28 +1,71 @@
|
|
1
1
|
module Hero
|
2
|
+
|
3
|
+
# Hero::Observer is designed to observe Hero::Formulas.
|
4
|
+
# It executes all registered steps whenever Hero::Formula#run is invoked.
|
5
|
+
# A Hero::Formula should only have 1 Hero::Observer attached.
|
2
6
|
class Observer
|
7
|
+
|
8
|
+
# The name of the Hero::Formula being observed.
|
3
9
|
attr_reader :formula_name
|
4
10
|
|
11
|
+
# @param [Symbol, String] formula_name The name of the Hero::Formula being observed.
|
5
12
|
def initialize(formula_name)
|
6
13
|
@formula_name = formula_name
|
7
14
|
end
|
8
15
|
|
16
|
+
# @return [Array] All registered steps.
|
9
17
|
def steps
|
10
|
-
@steps ||=
|
11
|
-
@steps.sort{ |a, b| a.last[:index] <=> b.last[:index] }.map{ |k, v| { k => v[:step] } }
|
18
|
+
@steps ||= []
|
12
19
|
end
|
13
20
|
|
21
|
+
# Adds a step to be executed when the Hero::Formula is run.
|
22
|
+
#
|
23
|
+
# @example A step must implement the interface
|
24
|
+
# def call(*args)
|
25
|
+
#
|
26
|
+
# @example More specifically
|
27
|
+
# def call(context, options={})
|
28
|
+
#
|
29
|
+
# The simplest example being a Proc.
|
30
|
+
#
|
31
|
+
# @note Steps are called in the order they are added. 1st in 1st called
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# add_step(:my_step) do |context, options|
|
35
|
+
# # logic here...
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# class MyStep
|
40
|
+
# def self.call(context, options={})
|
41
|
+
# # logic here...
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# add_step(:my_step, MyStep)
|
46
|
+
#
|
47
|
+
# @param [Symbol, String] name The name of the step.
|
48
|
+
# @param optional [Object] step The step to be executed.
|
49
|
+
# @yield optional [Object, Hash] Uses a passed block as the step to be executed.
|
14
50
|
def add_step(name, step=nil, &block)
|
15
|
-
|
51
|
+
steps.delete_if { |s| s.first == name }
|
16
52
|
step ||= block if block_given?
|
17
|
-
|
53
|
+
steps << [name, step]
|
18
54
|
end
|
19
55
|
|
56
|
+
# The callback triggered when Hero::Formula#run is invoked.
|
57
|
+
# This method runs all registered steps in order.
|
58
|
+
#
|
59
|
+
# @note A log message will be written to Hero.logger for each step that is called if Hero.logger has been set.
|
60
|
+
#
|
61
|
+
# @param [Object] context The context to be passed to each step.
|
62
|
+
# @param [Hash] options An option Hash to be passed to each step.
|
20
63
|
def update(context, options={})
|
21
64
|
steps.each do |step|
|
22
65
|
if Hero.logger
|
23
|
-
Hero.logger.info "HERO Formula: #{formula_name}, Step: #{step.
|
66
|
+
Hero.logger.info "HERO Formula: #{formula_name}, Step: #{step.first}, Context: #{context.inspect}, Options: #{options.inspect}"
|
24
67
|
end
|
25
|
-
step.
|
68
|
+
step.last.call(context, options)
|
26
69
|
end
|
27
70
|
end
|
28
71
|
|
data/spec/observer_spec.rb
CHANGED
@@ -8,8 +8,8 @@ describe "Hero::Observer instance" do
|
|
8
8
|
o = Hero::Observer.new(:example)
|
9
9
|
o.add_step(:one, step)
|
10
10
|
assert_equal o.steps.length, 1
|
11
|
-
assert_equal o.steps[0].
|
12
|
-
assert_equal o.steps[0].
|
11
|
+
assert_equal o.steps[0].first, :one
|
12
|
+
assert_equal o.steps[0].last, step
|
13
13
|
end
|
14
14
|
|
15
15
|
it "should support properly handle a double add" do
|
@@ -19,8 +19,8 @@ describe "Hero::Observer instance" do
|
|
19
19
|
o.add_step(:one, step1)
|
20
20
|
o.add_step(:one, step2)
|
21
21
|
assert_equal o.steps.length, 1
|
22
|
-
assert_equal o.steps[0].
|
23
|
-
assert_equal o.steps[0].
|
22
|
+
assert_equal o.steps[0].first, :one
|
23
|
+
assert_equal o.steps[0].last, step2
|
24
24
|
end
|
25
25
|
|
26
26
|
it "should properly sort steps based on the order they were added" do
|
@@ -31,9 +31,9 @@ describe "Hero::Observer instance" do
|
|
31
31
|
o.add_step(:four) {}
|
32
32
|
o.add_step(:one) {}
|
33
33
|
assert_equal o.steps.length, 4
|
34
|
-
assert_equal o.steps[0].
|
35
|
-
assert_equal o.steps[1].
|
36
|
-
assert_equal o.steps[2].
|
37
|
-
assert_equal o.steps[3].
|
34
|
+
assert_equal o.steps[0].first, :two
|
35
|
+
assert_equal o.steps[1].first, :three
|
36
|
+
assert_equal o.steps[2].first, :four
|
37
|
+
assert_equal o.steps[3].first, :one
|
38
38
|
end
|
39
39
|
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.
|
4
|
+
version: 0.0.5
|
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-25 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: ! ' Simplify your apps with Hero.
|
15
15
|
|