heed 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/History.md +26 -0
- data/LICENSE.txt +22 -0
- data/README.md +332 -0
- data/Rakefile +6 -0
- data/hark.gemspec +28 -0
- data/lib/hark.rb +11 -0
- data/lib/hark/ad_hoc.rb +48 -0
- data/lib/hark/core_ext.rb +10 -0
- data/lib/hark/dispatcher.rb +50 -0
- data/lib/hark/listener.rb +58 -0
- data/lib/hark/version.rb +3 -0
- data/spec/hark_spec.rb +172 -0
- data/spec/spec_helper.rb +9 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0345d1edb02b2cf539eec0ab3dd83d53b64ae55b
|
4
|
+
data.tar.gz: 6e49bd332ad4e1e77980a745ec3fa369fe2e263f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4ed74d4af4b784f28e102e3f9828aec13772078d5e4a2555019c1476385a6ecb85e8a9651f4b1b9060dd662673e419ebf60c3ff9ea8460101a45340f1093e03b
|
7
|
+
data.tar.gz: b5f84d357dad9563f3eca222f0dbcca63096dffea597640e4779f0ea2a16b11e8539b38a1e2b07b40bd531e73eccff6360ec0b871744f3a822b50cfaa305d069
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/History.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# History
|
2
|
+
|
3
|
+
## 0.0.7
|
4
|
+
|
5
|
+
* Change gem name to 'heed'
|
6
|
+
|
7
|
+
## 0.0.6
|
8
|
+
|
9
|
+
* remove Kernel#to_hark
|
10
|
+
|
11
|
+
## 0.0.5
|
12
|
+
|
13
|
+
* ruby 1.8.7 compat
|
14
|
+
|
15
|
+
## 0.0.4
|
16
|
+
|
17
|
+
* Adds Kernel#hearken
|
18
|
+
* Adds coveralls & History
|
19
|
+
|
20
|
+
## 0.0.3
|
21
|
+
|
22
|
+
* Adds ruby < 1.9 support
|
23
|
+
|
24
|
+
## 0.0.1 - 0.0.2
|
25
|
+
|
26
|
+
* Initial release, slims API down to Kernel#hark
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Ian White
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
# Hark
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/ianwhite-hark.png)](https://rubygems.org/gems/ianwhite-hark)
|
4
|
+
[![Build Status](https://travis-ci.org/ianwhite/hark.png)](https://travis-ci.org/ianwhite/hark)
|
5
|
+
[![Dependency Status](https://gemnasium.com/ianwhite/hark.png)](https://gemnasium.com/ianwhite/hark)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/ianwhite/hark.png)](https://codeclimate.com/github/ianwhite/hark)
|
7
|
+
[![Coverage Status](https://coveralls.io/repos/ianwhite/hark/badge.png)](https://coveralls.io/r/ianwhite/hark)
|
8
|
+
|
9
|
+
Create a ad-hoc listeners with hark.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'ianwhite-hark', :require => 'hark'
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install ianwhite-hark
|
24
|
+
|
25
|
+
## What & Why?
|
26
|
+
|
27
|
+
**hark** enables you to create a 'listener' object very easily. It's for programming in the *hexagonal* or *tell, don't ask* style.
|
28
|
+
The consumers of hark listeners don't know anything about hark. Because hark makes it easy to create ad-hoc object, it's easy to get
|
29
|
+
started with a tell-dont-ask style, in rails controllers for example. For more detail see the 'Rationale' section.
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Create a listener
|
34
|
+
|
35
|
+
To create a listener object use `hark`.
|
36
|
+
|
37
|
+
You can pass a symbol and block
|
38
|
+
|
39
|
+
hark :created do |user|
|
40
|
+
redirect_to(user, notice: "You have signed up!")
|
41
|
+
end
|
42
|
+
|
43
|
+
The following methods are more suitable for a listener with multiple messages.
|
44
|
+
|
45
|
+
A hash with callables as values
|
46
|
+
|
47
|
+
hark(
|
48
|
+
created: ->(user) { redirect_to(user, notice: "You have signed up!") },
|
49
|
+
invalid: ->(user) { @user = user; render "new" }
|
50
|
+
)
|
51
|
+
|
52
|
+
# assuming some methods for rendering and redirecting exist on the controller
|
53
|
+
hark(created: method(:redirect_to_user), invalid: method(:render_new))
|
54
|
+
|
55
|
+
Or, a 'respond_to' style block
|
56
|
+
|
57
|
+
hark do |on|
|
58
|
+
on.created {|user| redirect_to(user, notice: "You have signed up!") }
|
59
|
+
on.invalid {|user| @user = user; render "new" }
|
60
|
+
end
|
61
|
+
|
62
|
+
### Strict & lax listeners
|
63
|
+
|
64
|
+
By default, hark listeners are 'strict', they will only respond to the methods defined on them.
|
65
|
+
|
66
|
+
You create a 'lax' listener, responding to any message, by sending the `lax` message.
|
67
|
+
|
68
|
+
listener = hark(:foo) { "Foo" }
|
69
|
+
|
70
|
+
listener.bar
|
71
|
+
# => NoMethodError: undefined method `bar' for #<Hark::StrictListener:0x007fc91a03e568>
|
72
|
+
|
73
|
+
listener = listener.lax
|
74
|
+
listener.bar
|
75
|
+
# => []
|
76
|
+
|
77
|
+
To make a strict listener send the `strict` message.
|
78
|
+
|
79
|
+
### Combining listeners
|
80
|
+
|
81
|
+
Here are some ways of combining listeners.
|
82
|
+
|
83
|
+
# redirect listener
|
84
|
+
listener = hark(created: method(:redirect_to_user))
|
85
|
+
|
86
|
+
Add a message
|
87
|
+
|
88
|
+
listener = listener.hark :created do |user|
|
89
|
+
WelomeMailer.send_email(user)
|
90
|
+
end
|
91
|
+
|
92
|
+
Combine with another listener
|
93
|
+
|
94
|
+
logger = listener.hark(created: ->(u) { logger.info "User #{u} created" } )
|
95
|
+
listener = listener.hark(logger)
|
96
|
+
|
97
|
+
Combine with any object that support the same protocol
|
98
|
+
|
99
|
+
logger = UserLogger.new # responds to :created
|
100
|
+
listener = listener.hark(logger)
|
101
|
+
|
102
|
+
Turn any object into a listener, adding new methods as we go
|
103
|
+
|
104
|
+
hark UserLogger.new do |on|
|
105
|
+
on.created {|user| Emailer.send_welcom_email(user) }
|
106
|
+
end
|
107
|
+
|
108
|
+
Now, when listener is sent #created, all create handlers are called.
|
109
|
+
|
110
|
+
### Sugar: `Kernel#hearken`
|
111
|
+
|
112
|
+
Because of the precedence of the block operator, constructing ad-hoc listeners requires
|
113
|
+
you to insert some parens, which might be seen as unsightly, e.g:
|
114
|
+
|
115
|
+
seller.request_valuation(item, (hark do |on|
|
116
|
+
on.valuation_requested {|valuation| redirect_to valuation}
|
117
|
+
on.invalid_item {|item| redirect_to item, error: "Item not evaluable" }
|
118
|
+
end))
|
119
|
+
|
120
|
+
You may use Kernerl#hearken to create an ad-hoc listener using a passed block as follows
|
121
|
+
|
122
|
+
seller.hearken :request_valuation, item do |on|
|
123
|
+
on.valuation_requested {|valuation| redirect_to valuation}
|
124
|
+
on.invalid_item {|item| redirect_to item, error: "Item not evaluable" }
|
125
|
+
end
|
126
|
+
|
127
|
+
If you want to combine listeners with an ad-hoc block, you may pass a 0-arity block that is
|
128
|
+
yielded as the listener
|
129
|
+
|
130
|
+
seller.hearken :request_valuation, item do
|
131
|
+
hark valuation_notifier do |on|
|
132
|
+
on.valuation_requested {|valuation| redirect_to valuation}
|
133
|
+
on.invalid_item {|item| redirect_to item, error: "Item not evaluable" }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
### Return value
|
138
|
+
|
139
|
+
Using the return value of a listener is not encouraged. Hark is designed for a *tell, don't ask*
|
140
|
+
style of coding. That said the return value of a hark listener is an array of its handlers return values.
|
141
|
+
|
142
|
+
a = hark(:foo) { 'a' }
|
143
|
+
b = Object.new.tap {|o| o.singleton_class.send(:define_method, :foo) { 'b' } }
|
144
|
+
c = hark(foo: -> { 'c' }, bar: -> { 'c bar' })
|
145
|
+
|
146
|
+
a.foo # => ["a"]
|
147
|
+
hark(a,b).foo # => ["a", "b"]
|
148
|
+
hark(a,b,c).foo # => ["a", "b", "c"]
|
149
|
+
|
150
|
+
### Immutable
|
151
|
+
|
152
|
+
Hark listeners are immutable and `#lax`, `#strict`, and `#hark` all return new listeners.
|
153
|
+
|
154
|
+
## Rationale
|
155
|
+
|
156
|
+
When programming in the 'tell-dont-ask' or 'hexagonal' style, program flow is managed by passing listener, or
|
157
|
+
response, objects to service objects, which call back depending on what happened. This allows logic that is concerned with the caller's domain to remain isolated from the service object.
|
158
|
+
|
159
|
+
The idea behind **hark** is that there should be little ceremony involved in the listener/response mechanics, and
|
160
|
+
that simple listeners can easily be refactored into objects in their own right, without changing the protocols between
|
161
|
+
the calling and servcie objects.
|
162
|
+
|
163
|
+
To that end, service objects should not know anything other than the listener/response protocol, and shouldn't have to 'publish' their
|
164
|
+
results beyond a simple method call.
|
165
|
+
|
166
|
+
As a simple example, a user creation service object defines a response protocol as follows:
|
167
|
+
|
168
|
+
* created_user(user) _the user was succesfully created_
|
169
|
+
* invalid_user(user) _the user couldn't be created because it was invalid_
|
170
|
+
|
171
|
+
The UserCreator object's main method will have some code as follows:
|
172
|
+
|
173
|
+
if # some logic that means the user params were valid and we could persist the user
|
174
|
+
response.created_user(user)
|
175
|
+
else
|
176
|
+
response.invalid_user(user)
|
177
|
+
end
|
178
|
+
|
179
|
+
Let's say a controller is calling this, and you are using hark. In the beginning you would do something like this:
|
180
|
+
|
181
|
+
def create
|
182
|
+
user_creator.call(user_params, hark do |on|
|
183
|
+
on.created_user {|user| redirect_to user, notice: "Welome!" }
|
184
|
+
on.invalid_user {|user| @user = user; render "new" }
|
185
|
+
end)
|
186
|
+
end
|
187
|
+
|
188
|
+
This keeps the controller's handling of the user creation nicely separate from the saving of the user creator.
|
189
|
+
|
190
|
+
Then, a requirement comes in to log the creation of users. The first attempt might be this:
|
191
|
+
|
192
|
+
def create
|
193
|
+
user_creator.call(user_params, hark do |on|
|
194
|
+
on.created_user do |user|
|
195
|
+
redirect_to user, notice: "Welome!"
|
196
|
+
logger.info "User #{user} created"
|
197
|
+
end
|
198
|
+
on.invalid_user {|user| @user = user; render "new" }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
Then a requirement comes in to email users on succesful creation, there's an UserEmailer that responds
|
203
|
+
to the same protocol. Also, the UX team want to log invalid users.
|
204
|
+
|
205
|
+
There's quite a lot going on now, we can tie it up as follows:
|
206
|
+
|
207
|
+
def create
|
208
|
+
response = hark(ui_response, UserEmailer.new, ux_team_response)
|
209
|
+
user_creator.call user_params, response
|
210
|
+
end
|
211
|
+
|
212
|
+
# UserEmailer responds to #created_user(user)
|
213
|
+
|
214
|
+
def ui_response
|
215
|
+
hark do |on|
|
216
|
+
on.created_user {|user| redirect_to user, notice: "Welome!" }
|
217
|
+
on.invalid_user {|user| @user = user; render "new" }
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def ux_team_response
|
222
|
+
hark(:invalid_user) {|user| logger.info("User invalid: #{user}") }
|
223
|
+
end
|
224
|
+
|
225
|
+
If some of the response code gets hairy, we can easily swap out hark ad-hoc objects for 'proper' ones.
|
226
|
+
For example, the UI response might get a bit hairy, and so we make a new object.
|
227
|
+
|
228
|
+
def create
|
229
|
+
response = hark(UiResponse.new(self), UserEmailer.new, ux_team_response)
|
230
|
+
user_creator.call user_params, response
|
231
|
+
end
|
232
|
+
|
233
|
+
class UiResponse < SimpleDelegator
|
234
|
+
def created_user user
|
235
|
+
if request.format.json?
|
236
|
+
# ...
|
237
|
+
else
|
238
|
+
# ...
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def invalid_user user
|
243
|
+
# ...
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
Note that throughout this process we didn't have to modify the UserCreator code, even when we transitioned
|
248
|
+
to/from hark for different repsonses/styles.
|
249
|
+
|
250
|
+
### Testing your listeners
|
251
|
+
|
252
|
+
Don't pay any attention to hark when you're testing, hark is just a utility to create listeners, and so what
|
253
|
+
you should be testing is the protocol.
|
254
|
+
|
255
|
+
For example the service object tests will test functionality that pertains to the actual creation of the user,
|
256
|
+
and will test that the correct message is sent to the response in those circumstances. Whereas the controller tests
|
257
|
+
will mock out the service object, and test what happens when the service object sends the messages to the response as
|
258
|
+
dictated by the protocol.
|
259
|
+
|
260
|
+
describe UserCreator do
|
261
|
+
let(:service) { described_class.new }
|
262
|
+
|
263
|
+
describe "#call params, response" do
|
264
|
+
subject { service.call params, response }
|
265
|
+
|
266
|
+
let(:response) { double }
|
267
|
+
|
268
|
+
context "when the user succesfully saves"
|
269
|
+
let(:params) { {name: "created user", # and other successful user params }
|
270
|
+
|
271
|
+
it "sends #created_user to the response with the created user" do
|
272
|
+
response.should_receive(:created_user) do |user|
|
273
|
+
user.name.should == "created user"
|
274
|
+
end
|
275
|
+
subject
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
context "when the user succesfully saves"
|
280
|
+
let(:params) { {name: "invalid user", # and invalid user params }
|
281
|
+
|
282
|
+
it "sends #invalid_user to the response with the created user" do
|
283
|
+
response.should_receive(:invalid_user) do |user|
|
284
|
+
# test that the object passed is the invalid user
|
285
|
+
end
|
286
|
+
subject
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
describe NewUserController do
|
293
|
+
before { controller.stub(user_creator: user_creator) } # or some other sensible way of injecting a fake user_creator
|
294
|
+
|
295
|
+
let(:user_creator) { double "User creator" }
|
296
|
+
let(:user) { double "A user" }
|
297
|
+
|
298
|
+
context "when the user_creator is succesful" do
|
299
|
+
before do
|
300
|
+
user_creator.stub :call do |params, response|
|
301
|
+
response.created_user(user)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
it "should redirect to the user"
|
306
|
+
|
307
|
+
it "should email the user"
|
308
|
+
|
309
|
+
it "should log the creation of the user"
|
310
|
+
end
|
311
|
+
|
312
|
+
context "when the user_creator says the params are invalid" do
|
313
|
+
before do
|
314
|
+
user_creator.stub :call do |params, response|
|
315
|
+
response.invalid_user(user)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
it "should render new with the user"
|
320
|
+
|
321
|
+
it "should log something for the UX team"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
## Contributing
|
327
|
+
|
328
|
+
1. Fork it
|
329
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
330
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
331
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
332
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/hark.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hark/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "heed"
|
8
|
+
spec.version = Hark::VERSION
|
9
|
+
spec.authors = ["Ian White"]
|
10
|
+
spec.email = ["ian.w.white@gmail.com"]
|
11
|
+
spec.description = %q{Create ad-hoc listener objects with impunity}
|
12
|
+
spec.summary = %q{Hark is a gem that enables writing code in a "hexagonal architecture" or "tell don't ask" style}
|
13
|
+
spec.homepage = "http://github.com/ianwhite/hark"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
|
25
|
+
if RUBY_VERSION > "1.9"
|
26
|
+
spec.add_development_dependency "coveralls"
|
27
|
+
end
|
28
|
+
end
|
data/lib/hark.rb
ADDED
data/lib/hark/ad_hoc.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Hark
|
2
|
+
# AdHoc is a tiny class to facilitate creating an ad-hoc object that from either a hash or proc.
|
3
|
+
#
|
4
|
+
# Eg. from a hash:
|
5
|
+
#
|
6
|
+
# handler = AdHoc.new(success: (o)-> { o.great_success }, failure: (o)-> { o.failed } )
|
7
|
+
#
|
8
|
+
# Eg. from a 'response' style block:
|
9
|
+
#
|
10
|
+
# handler = AdHoc.new do |on|
|
11
|
+
# on.success {|o| o.great_success }
|
12
|
+
# on.failure {|o| o.failed }
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# Eg. adding methods after creation
|
16
|
+
#
|
17
|
+
# obj = AdHoc.new
|
18
|
+
# obj.add_method!(:foo) { "bar" }
|
19
|
+
#
|
20
|
+
# All blocks keep their original binding. This makes AdHoc suitable for creating
|
21
|
+
# ad-hoc responses from controller type objects.
|
22
|
+
#
|
23
|
+
class AdHoc
|
24
|
+
def self.new hash = {}, &proc
|
25
|
+
super().tap do |ad_hoc|
|
26
|
+
AddMethodsFromProc.new(proc, ad_hoc) if block_given?
|
27
|
+
hash.each {|method, body| ad_hoc.add_method!(method, &body) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_method!(method, &body)
|
32
|
+
singleton_class = class << self; self; end
|
33
|
+
singleton_class.send(:define_method, method) {|*args, &block| body.call(*args, &block) }
|
34
|
+
end
|
35
|
+
|
36
|
+
class AddMethodsFromProc
|
37
|
+
def initialize proc, ad_hoc
|
38
|
+
@ad_hoc = ad_hoc
|
39
|
+
proc.call(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing method, *, &body
|
43
|
+
@ad_hoc.add_method!(method, &body)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Hark
|
2
|
+
class Dispatcher
|
3
|
+
# from(:success) do
|
4
|
+
# "success"
|
5
|
+
# end
|
6
|
+
#
|
7
|
+
# from(success: ->{ "success" })
|
8
|
+
#
|
9
|
+
# from do |on|
|
10
|
+
# on.success { "success" }
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
def self.from(*args, &block)
|
14
|
+
if block
|
15
|
+
args << (args.last.is_a?(Symbol) ? {args.pop => block} : block)
|
16
|
+
end
|
17
|
+
|
18
|
+
new args.map{|o| to_handler(o) }.flatten.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.to_handler object
|
22
|
+
case object
|
23
|
+
when Listener then object.dispatcher.handlers
|
24
|
+
when Dispatcher then object.handlers
|
25
|
+
when Hash then AdHoc.new(object)
|
26
|
+
when Proc then AdHoc.new(&object)
|
27
|
+
else object
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :handlers
|
32
|
+
|
33
|
+
def initialize handlers
|
34
|
+
@handlers = handlers
|
35
|
+
freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
def handles? method
|
39
|
+
handlers.any? {|handler| handler.respond_to?(method) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle method, *args, &block
|
43
|
+
results = []
|
44
|
+
handlers.each do |handler|
|
45
|
+
results << handler.send(method, *args, &block) if handler.respond_to?(method)
|
46
|
+
end
|
47
|
+
results
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hark
|
2
|
+
# A Listener holds a dispatcher, which it dispatches messages to
|
3
|
+
#
|
4
|
+
# A listener is by default a 'strict' listener, it will raise NoMethodError if
|
5
|
+
# it is sent a message it doesn't know how to handle.
|
6
|
+
#
|
7
|
+
# A listener can be turned into a 'lax' listener, by sending it the #lax message.
|
8
|
+
# A lax listener will silently swallow any unknown messages.
|
9
|
+
class Listener
|
10
|
+
def self.new *args, &block
|
11
|
+
self == Listener ? StrictListener.new(*args, &block) : super(*args, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :dispatcher
|
15
|
+
|
16
|
+
def initialize(*args, &block)
|
17
|
+
@dispatcher = Dispatcher.from(*args, &block)
|
18
|
+
freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def strict
|
22
|
+
StrictListener.new dispatcher
|
23
|
+
end
|
24
|
+
|
25
|
+
def lax
|
26
|
+
LaxListener.new dispatcher
|
27
|
+
end
|
28
|
+
|
29
|
+
def hark *args, &block
|
30
|
+
self.class.new dispatcher, *args, &block
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class StrictListener < Listener
|
35
|
+
def respond_to?(method, *args)
|
36
|
+
super || dispatcher.handles?(method)
|
37
|
+
end
|
38
|
+
|
39
|
+
def method_missing *args, &block
|
40
|
+
results = dispatcher.handle(*args, &block)
|
41
|
+
if results.length > 0
|
42
|
+
results
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class LaxListener < Listener
|
50
|
+
def respond_to? *args
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def method_missing *args, &block
|
55
|
+
dispatcher.handle(*args, &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/hark/version.rb
ADDED
data/spec/hark_spec.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'hark'
|
3
|
+
|
4
|
+
describe Hark do
|
5
|
+
let(:transcript) { [] }
|
6
|
+
|
7
|
+
class PlainListener < Struct.new(:transcript)
|
8
|
+
def success(value)
|
9
|
+
transcript.push [:succeeded, value]
|
10
|
+
end
|
11
|
+
|
12
|
+
def failure(value)
|
13
|
+
transcript.push [:failed, value]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
shared_examples_for "a success/failure listener" do
|
18
|
+
describe "success" do
|
19
|
+
before { listener.success(42) }
|
20
|
+
it { transcript.should == [[:succeeded, 42]] }
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "failure" do
|
24
|
+
before { listener.failure(54) }
|
25
|
+
it { transcript.should == [[:failed, 54]] }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
shared_examples_for "a strict listener" do
|
30
|
+
it { strict_listener.should_not respond_to(:unknown) }
|
31
|
+
it { expect{ strict_listener.unknown }.to raise_error(NoMethodError) }
|
32
|
+
end
|
33
|
+
|
34
|
+
shared_examples_for "a lax listener" do
|
35
|
+
it { lax_listener.should respond_to(:unknown) }
|
36
|
+
it { lax_listener.unknown.should == [] }
|
37
|
+
end
|
38
|
+
|
39
|
+
shared_examples_for "a success/failure hark listener" do
|
40
|
+
it_should_behave_like "a success/failure listener"
|
41
|
+
it_should_behave_like "a strict listener" do
|
42
|
+
let(:strict_listener) { listener }
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when made lax" do
|
46
|
+
let(:lax_listener) { listener.lax }
|
47
|
+
it_should_behave_like "a lax listener"
|
48
|
+
|
49
|
+
context "and made strict again" do
|
50
|
+
let(:strict_listener) { lax_listener.strict }
|
51
|
+
it_should_behave_like "a strict listener"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "A plain (non hark) listener object" do
|
57
|
+
let(:listener) { PlainListener.new(transcript) }
|
58
|
+
|
59
|
+
it_should_behave_like "a success/failure listener"
|
60
|
+
it_should_behave_like "a strict listener" do
|
61
|
+
let(:strict_listener) { listener }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "hark with respond_to style block" do
|
66
|
+
let(:listener) do
|
67
|
+
hark do |on|
|
68
|
+
on.success {|v| transcript.push [:succeeded, v] }
|
69
|
+
on.failure {|v| transcript.push [:failed, v] }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it_should_behave_like "a success/failure hark listener"
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "hark with callables" do
|
77
|
+
let(:listener) do
|
78
|
+
hark :success => lambda{|v| transcript.push [:succeeded, v] }, :failure => lambda{|v| transcript.push [:failed, v] }
|
79
|
+
end
|
80
|
+
|
81
|
+
it_should_behave_like "a success/failure hark listener"
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "hark built up in steps" do
|
85
|
+
let(:listener) do
|
86
|
+
l = hark
|
87
|
+
l = l.hark(:success) {|v| transcript.push [:succeeded, v] }
|
88
|
+
l = l.hark(:failure) {|v| transcript.push [:failed, v] }
|
89
|
+
end
|
90
|
+
|
91
|
+
it_should_behave_like "a success/failure hark listener"
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#hark(object)" do
|
95
|
+
let(:listener) { hark PlainListener.new(transcript) }
|
96
|
+
|
97
|
+
it_should_behave_like "a success/failure hark listener"
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "combine two listeners together" do
|
101
|
+
let(:logger) { hark(:signup_user) {|user| transcript << "User #{user} signed up" } }
|
102
|
+
let(:emailer) { hark(:signup_user) {|user| transcript << "Emailed #{user}" } }
|
103
|
+
|
104
|
+
shared_examples_for "combined listeners" do
|
105
|
+
before { listener.signup_user("Fred") }
|
106
|
+
|
107
|
+
it { transcript.should == ["User Fred signed up", "Emailed Fred"] }
|
108
|
+
end
|
109
|
+
|
110
|
+
it_behaves_like "combined listeners" do
|
111
|
+
let(:listener) { logger.hark(emailer) }
|
112
|
+
end
|
113
|
+
|
114
|
+
it_behaves_like "combined listeners" do
|
115
|
+
let(:listener) { hark(logger, emailer) }
|
116
|
+
end
|
117
|
+
|
118
|
+
it_behaves_like "combined listeners" do
|
119
|
+
let(:listener) do
|
120
|
+
hark logger do |on|
|
121
|
+
on.signup_user {|user| transcript << "Emailed #{user}" }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "lax/strict is preserved on #hark" do
|
128
|
+
it { hark.lax.hark.should be_a Hark::LaxListener }
|
129
|
+
it { hark.strict.hark.should be_a Hark::StrictListener }
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "when methods return falsy" do
|
133
|
+
let(:listener) { hark(:foo) { false } }
|
134
|
+
|
135
|
+
it { expect{ listener.foo }.to_not raise_error }
|
136
|
+
it { listener.foo.should == [false] }
|
137
|
+
end
|
138
|
+
|
139
|
+
describe "#hearken :method" do
|
140
|
+
let(:object) do
|
141
|
+
Object.new.tap do |obj|
|
142
|
+
class << obj
|
143
|
+
def foo arg1, arg2, listener
|
144
|
+
listener.foo(arg1)
|
145
|
+
listener.bar(arg2)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context "with 1 arity block" do
|
152
|
+
it "sends :method with an ad-hoc listener created from the block" do
|
153
|
+
object.hearken :foo, "ONE", "TWO" do |on|
|
154
|
+
on.foo {|a| transcript << [:foo, a] }
|
155
|
+
on.bar {|a| transcript << [:bar, a] }
|
156
|
+
end
|
157
|
+
transcript.should == [[:foo, "ONE"], [:bar, "TWO"]]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "with 0 arity block" do
|
162
|
+
it "sends :method with listener created by yielding to the block" do
|
163
|
+
foo = hark(:foo) {|a| transcript << [:foo, a] }
|
164
|
+
|
165
|
+
object.hearken :foo, "ONE", "TWO" do
|
166
|
+
hark(foo, :bar) {|a| transcript << [:bar, a] }
|
167
|
+
end
|
168
|
+
transcript.should == [[:foo, "ONE"], [:bar, "TWO"]]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: heed
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian White
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: coveralls
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Create ad-hoc listener objects with impunity
|
70
|
+
email:
|
71
|
+
- ian.w.white@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .coveralls.yml
|
77
|
+
- .gitignore
|
78
|
+
- .travis.yml
|
79
|
+
- Gemfile
|
80
|
+
- History.md
|
81
|
+
- LICENSE.txt
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- hark.gemspec
|
85
|
+
- lib/hark.rb
|
86
|
+
- lib/hark/ad_hoc.rb
|
87
|
+
- lib/hark/core_ext.rb
|
88
|
+
- lib/hark/dispatcher.rb
|
89
|
+
- lib/hark/listener.rb
|
90
|
+
- lib/hark/version.rb
|
91
|
+
- spec/hark_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
homepage: http://github.com/ianwhite/hark
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 2.0.6
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: Hark is a gem that enables writing code in a "hexagonal architecture" or
|
117
|
+
"tell don't ask" style
|
118
|
+
test_files:
|
119
|
+
- spec/hark_spec.rb
|
120
|
+
- spec/spec_helper.rb
|