siftery-wisper 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +22 -0
  5. data/CHANGELOG.md +129 -0
  6. data/CONTRIBUTING.md +61 -0
  7. data/Gemfile +13 -0
  8. data/README.md +373 -0
  9. data/Rakefile +10 -0
  10. data/bin/console +8 -0
  11. data/bin/setup +6 -0
  12. data/gem-public_cert.pem +21 -0
  13. data/lib/wisper.rb +65 -0
  14. data/lib/wisper/broadcasters/logger_broadcaster.rb +37 -0
  15. data/lib/wisper/broadcasters/send_broadcaster.rb +9 -0
  16. data/lib/wisper/configuration.rb +44 -0
  17. data/lib/wisper/global_listeners.rb +74 -0
  18. data/lib/wisper/publisher.rb +89 -0
  19. data/lib/wisper/registration/block.rb +11 -0
  20. data/lib/wisper/registration/object.rb +77 -0
  21. data/lib/wisper/registration/registration.rb +19 -0
  22. data/lib/wisper/temporary_listeners.rb +41 -0
  23. data/lib/wisper/value_objects/events.rb +61 -0
  24. data/lib/wisper/value_objects/prefix.rb +29 -0
  25. data/lib/wisper/version.rb +3 -0
  26. data/siftery-wisper.gemspec +31 -0
  27. data/spec/lib/global_listeners_spec.rb +82 -0
  28. data/spec/lib/integration_spec.rb +56 -0
  29. data/spec/lib/simple_example_spec.rb +21 -0
  30. data/spec/lib/temporary_global_listeners_spec.rb +67 -0
  31. data/spec/lib/wisper/broadcasters/logger_broadcaster_spec.rb +93 -0
  32. data/spec/lib/wisper/broadcasters/send_broadcaster_spec.rb +28 -0
  33. data/spec/lib/wisper/configuration/broadcasters_spec.rb +11 -0
  34. data/spec/lib/wisper/configuration_spec.rb +36 -0
  35. data/spec/lib/wisper/publisher_spec.rb +331 -0
  36. data/spec/lib/wisper/registrations/object_spec.rb +14 -0
  37. data/spec/lib/wisper/value_objects/events_spec.rb +107 -0
  38. data/spec/lib/wisper/value_objects/prefix_spec.rb +46 -0
  39. data/spec/lib/wisper_spec.rb +99 -0
  40. data/spec/spec_helper.rb +24 -0
  41. metadata +101 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4f6bf677ec7224bac38caa11cb49620dedb6a0e2d28095b940bc0f6e1563d54b
4
+ data.tar.gz: 7007c32fce8fdc5ceebb740c965210dab06cd4ba12ff6e639d671e705252d296
5
+ SHA512:
6
+ metadata.gz: 0b39cec008bc8bae5b15e560a225b99afda15db75ebffdb81602642ca6734636fe8d903958ed2dd0a187926643a55c06a0ed7f2daa76180b0b4889d35fdf2401
7
+ data.tar.gz: 03dd49ce611320e862a6af823e8a58eaad516d8d336eae5aafedd60db7d2f96555e801c0db05f241d3dbb92125e3172b7000dc64fcee532414e34f8736949f04
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .ruby-version
4
+ .ruby-gemspec
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ tags
21
+ vendor
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format progress
3
+ --require spec_helper
4
+ --warnings
@@ -0,0 +1,22 @@
1
+ language: ruby
2
+ before_install:
3
+ - gem update --system
4
+ - gem update bundler
5
+ bundler_args: --without=extras
6
+ script: rspec spec
7
+ sudo: false
8
+ rvm:
9
+ - '2.1.10'
10
+ - '2.2.6'
11
+ - '2.3.3'
12
+ - '2.4.0'
13
+ - jruby-9.1.6.0
14
+ # - rbx-2
15
+ ### ALLOWED FAILURES ###
16
+ # see how compatible we are with dev versions, but do not fail the build
17
+ - ruby-head
18
+ - jruby-head
19
+ matrix:
20
+ allow_failures:
21
+ - rvm: ruby-head
22
+ - rvm: jruby-head
@@ -0,0 +1,129 @@
1
+ ## HEAD (unreleased)
2
+
3
+ ## 2.0.0 (7th Mar 2017)
4
+
5
+ Authors: Sergey Mostovoy, Andrew Kozin, Kyle Tolle, Martin, Rob Miller, Mike
6
+ Dalto, orthographic-pedant, Drew Ulmer, Mikey Hogarth, Attila Domokos, Josh
7
+ Miltz, Pascal Betz, Vasily Kolesnikov, Julien Letessier, Kris Leech
8
+
9
+ * Fix: logger raises exception if hash is passed as an argument to a listener #133, #136
10
+ * Fix: deprecation warnings #120
11
+ * Doc improvements: #106, #111, #116, #122, #128, #130, #147, #149, #150, #151
12
+ * Adds: Allow configuration of default prefix when using `prefix: true`. #105
13
+ * Adds: Allow unsubscribing of global listeners #118
14
+ * Adds: Helpful error message when passing a block to `#subscribe` of `#on` #125
15
+ * Adds: raise an error message when `#on` is not passed a block #146
16
+ * Adds: Support for JRuby 9.x #148
17
+ * Adds: Support for MRI 2.4.0 #155
18
+ * Refactor specs #126, #131
19
+
20
+ ## 2.0.0.rc1 (17 Dec 2014)
21
+
22
+ Authors: Kris Leech
23
+
24
+ * remove: deprecated methods
25
+ * remove: rspec matcher and stubbing (moved to [wisper-rspec](https://github.com/krisleech/wisper-rspec))
26
+ * feature: add regexp support to `on` argument
27
+ * remove: announce alias for broadcasting
28
+ * docs: add Code of Conduct
29
+ * drop support for Ruby 1.9
30
+
31
+ ## 1.6.0 (25 Oct 2014)
32
+
33
+ Authors: Kris Leech
34
+
35
+ * deprecate: add_listener, add_block_listener and respond_to
36
+ * internal: make method naming more consistent
37
+
38
+ ## 1.5.0 (6th Oct 2014)
39
+
40
+ Authors: Kris Leech
41
+
42
+ * feature: allow events to be published asynchronously
43
+ * feature: broadcasting of events is plugable and configurable
44
+ * feature: broadcasters can be aliased via a symbol
45
+ * feature: logging broadcaster
46
+
47
+ ## 1.4.0 (8th Sept 2014)
48
+
49
+ Authors: Kris Leech, Marc Ignacio, Ahmed Abdel Razzak, kmehkeri, Jake Hoffner
50
+
51
+ * feature: matcher for rspec 3
52
+ * fix: temporary global listeners are cleared if an exception is raised
53
+ * refactor: update all specs to rspec 3 expect syntax
54
+ * docs: update README to rspec 3 expect syntax
55
+ * feature: combine global and temporary listener methods as `Wisper.subscribe`
56
+ * deprecate: `Wisper.add_listener` and `Wisper.with_listeners,` use `Wisper.subscribe` instead
57
+
58
+ ## 1.3.0 (18th Jan 2014)
59
+
60
+ Authors: Kris Leech, Yan Pritzker, Charlie Tran
61
+
62
+ * feature: global subscriptions can be scoped to a class (and sub-classes)
63
+ * upgrade: use rspec 3
64
+ * feature: allow prefixing of events with 'on'
65
+ * feature: Allow stubbed publisher method to accept arbitrary args
66
+
67
+ ## 1.2.1 (7th Oct 2013)
68
+
69
+ Authors: Kris Leech, Tomasz Szymczyszyn, Alex Heeton
70
+
71
+ * feature: global subscriptions can be passed options
72
+ * docs: improve README examples
73
+ * docs: add license to gemspec
74
+
75
+ ## 1.2.0 (21st July 2013)
76
+
77
+ Authors: Kris Leech, Darren Coxall
78
+
79
+ * feature: support for multiple events at once
80
+ * fix: clear global listeners after each spec
81
+
82
+ ## 1.1.0 (7th June 2013)
83
+
84
+ Authors: Kris Leech, chatgris
85
+
86
+ * feature: add temporary global listeners
87
+ * docs: improve ActiveRecord example
88
+ * refactor: improve specs
89
+ * upgrade: add Ruby 2.0 support
90
+ * fix: make listener collection immutable
91
+ * remove: async publishing and Celluloid dependency
92
+ * fix: Make global listeners getter and setter threadsafe [9]
93
+
94
+ ## 1.0.1 (2nd May 2013)
95
+
96
+ Authors: Kris Leech, Yan Pritzker
97
+
98
+ * feature: add async publishing using Celluloid
99
+ * docs: improve README examples
100
+ * feature: `stub_wisper_publisher` rspec helper
101
+ * feature: global listeners
102
+ * refactor: improve specs
103
+
104
+ ## 1.0.0 (7th April 2013)
105
+
106
+ Authors: Kris Leech
107
+
108
+ * refactor: specs
109
+ * refactor: registrations
110
+ * feature: Add `with` argument to `subscribe`
111
+ * docs: improve README examples
112
+ * feature: Allow subscriptions to be chainable
113
+ * feature: Add `on` syntax for block subscription
114
+ * remove: Remove support for Ruby 1.8.7
115
+ * docs: Add badges to README
116
+
117
+ ## 0.0.2 (30th March 2013)
118
+
119
+ Authors: Kris Leech
120
+
121
+ * remove: ActiveSupport dependency
122
+ * docs: fix syntax highlighting in README
123
+
124
+ ## 0.0.1 (30th March 2013)
125
+
126
+ Authors: Kris Leech
127
+
128
+ * docs: add README
129
+ * feature: registration of objects and blocks
@@ -0,0 +1,61 @@
1
+ # How to Contribute
2
+
3
+ We very much welcome contributions to Wisper.
4
+
5
+ This project is intended to be a safe, welcoming space for collaboration,
6
+ and contributors are expected to adhere to the
7
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
8
+
9
+ ## Getting started
10
+
11
+ Please first check the existing [Issues](https://github.com/krisleech/wisper/issues)
12
+ and [Pull Requests](https://github.com/krisleech/wisper/pulls) to ensure your
13
+ issue has not already been discused.
14
+
15
+ ## Bugs
16
+
17
+ Please submit a bug report to the issue tracker, with the version of Wisper
18
+ and Ruby you are using and a small code sample (or better yet a failing test).
19
+
20
+ ## Features
21
+
22
+ Please open an issue with your proposed feature. We can discuss the feature and
23
+ if it is acceptable we can also discuss implimentation details. You will in
24
+ most cases have to submit a PR which adds the feature.
25
+
26
+ Wisper is a micro library and will remain lean. Some features would be most
27
+ appropriate as an extension to Wisper.
28
+
29
+ We also have a [Gitter channel](https://gitter.im/krisleech/wisper) if you wish to discuss your ideas.
30
+
31
+ ## Backlog / Roadmap
32
+
33
+ The backlog for Wisper and related gems is on [Waffle](https://waffle.io/krisleech/wisper).
34
+
35
+ ## Questions
36
+
37
+ Try the [Wiki](https://github.com/krisleech/wisper/wiki) first, the examples
38
+ and how to sections have lots of information.
39
+
40
+ Please ask questions on StackOverflow, [tagged wisper](https://stackoverflow.com/questions/tagged/wisper).
41
+
42
+ Feel free to ping me the URL on [Twitter](https://twitter.com/krisleech).
43
+
44
+ ## Pull requests
45
+
46
+ * Fork the project, create a new branch from `v1` or `master`.
47
+ * Squash commits which are related.
48
+ * Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
49
+ * Documentation only changes should have `[skip ci]` in the commit message
50
+ * Follow existing code style in terms of syntax, indentation etc.
51
+ * Add an entry to the CHANGELOG
52
+ * Do not bump the VERSION, but do indicate in the CHANGELOG if the change is
53
+ not backwards compatible.
54
+ * Issue a Pull Request
55
+
56
+ ## Versions
57
+
58
+ The `v1` branch is a long lived branch and you should
59
+ branch from this if you wish to fix an issue in version `~> 1.0`.
60
+
61
+ The `master` branch will target the next version of Wisper.
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'rake'
6
+ gem 'rspec'
7
+ gem 'coveralls', require: false
8
+
9
+ group :extras do
10
+ gem 'flay'
11
+ gem 'pry'
12
+ gem 'yard'
13
+ end
@@ -0,0 +1,373 @@
1
+ # Wisper
2
+
3
+ *A micro library providing Ruby objects with Publish-Subscribe capabilities*
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/wisper.svg)](http://badge.fury.io/rb/wisper)
6
+ [![Code Climate](https://codeclimate.com/github/krisleech/wisper.svg)](https://codeclimate.com/github/krisleech/wisper)
7
+ [![Build Status](https://travis-ci.org/krisleech/wisper.svg?branch=master)](https://travis-ci.org/krisleech/wisper)
8
+ [![Coverage Status](https://coveralls.io/repos/krisleech/wisper/badge.svg?branch=master)](https://coveralls.io/r/krisleech/wisper?branch=master)
9
+
10
+ * Decouple core business logic from external concerns in Hexagonal style architectures
11
+ * Use as an alternative to ActiveRecord callbacks and Observers in Rails apps
12
+ * Connect objects based on context without permanence
13
+ * React to events synchronously or asynchronously
14
+
15
+ Note: Wisper was originally extracted from a Rails codebase but is not dependant on Rails.
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'wisper', '2.0.0'
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Any class with the `Wisper::Publisher` module included can broadcast events
28
+ to subscribed listeners. Listeners subscribe, at runtime, to the publisher.
29
+
30
+ ### Publishing
31
+
32
+ ```ruby
33
+ class CancelOrder
34
+ include Wisper::Publisher
35
+
36
+ def call(order_id)
37
+ order = Order.find_by_id(order_id)
38
+
39
+ # business logic...
40
+
41
+ if order.cancelled?
42
+ broadcast(:cancel_order_successful, order.id)
43
+ else
44
+ broadcast(:cancel_order_failed, order.id)
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ When a publisher broadcasts an event it can include any number of arguments.
51
+
52
+ The `broadcast` method is also aliased as `publish`.
53
+
54
+ You can also include `Wisper.publisher` instead of `Wisper::Publisher`.
55
+
56
+ ### Subscribing
57
+
58
+ #### Objects
59
+
60
+ Any object can be subscribed as a listener.
61
+
62
+ ```ruby
63
+ cancel_order = CancelOrder.new
64
+
65
+ cancel_order.subscribe(OrderNotifier.new)
66
+
67
+ cancel_order.call(order_id)
68
+ ```
69
+
70
+ The listener would need to implement a method for every event it wishes to receive.
71
+
72
+ ```ruby
73
+ class OrderNotifier
74
+ def cancel_order_successful(order_id)
75
+ order = Order.find_by_id(order_id)
76
+
77
+ # notify someone ...
78
+ end
79
+ end
80
+ ```
81
+
82
+ #### Blocks
83
+
84
+ Blocks can be subscribed to single events and can be chained.
85
+
86
+ ```ruby
87
+ cancel_order = CancelOrder.new
88
+
89
+ cancel_order.on(:cancel_order_successful) { |order_id| ... }
90
+ .on(:cancel_order_failed) { |order_id| ... }
91
+
92
+ cancel_order.call(order_id)
93
+ ```
94
+
95
+ You can also subscribe to multiple events using `on` by passing
96
+ additional events as arguments.
97
+
98
+ ```ruby
99
+ cancel_order = CancelOrder.new
100
+
101
+ cancel_order.on(:cancel_order_successful) { |order_id| ... }
102
+ .on(:cancel_order_failed,
103
+ :cancel_order_invalid) { |order_id| ... }
104
+
105
+ cancel_order.call(order_id)
106
+ ```
107
+
108
+ Do not `return` from inside a subscribed block, due to the way
109
+ [Ruby treats blocks](http://product.reverb.com/2015/02/28/the-strange-case-of-wisper-and-ruby-blocks-behaving-like-procs/)
110
+ this will prevent any subsequent listeners having their events delivered.
111
+
112
+ ### Handling Events Asynchronously
113
+
114
+ ```ruby
115
+ cancel_order.subscribe(OrderNotifier.new, async: true)
116
+ ```
117
+
118
+ You can also pass additional configuration options to the job using the following syntax:
119
+
120
+ ```ruby
121
+ cancel_order.subscribe(OrderNotifier.new, async: { queue: 'custom', retry: false })
122
+ ```
123
+
124
+ The list of supported handler options is determined by adapter
125
+
126
+ Wisper has various adapters for asynchronous event handling, please refer to
127
+ [wisper-celluloid](https://github.com/krisleech/wisper-celluloid),
128
+ [wisper-sidekiq](https://github.com/krisleech/wisper-sidekiq),
129
+ [wisper-activejob](https://github.com/krisleech/wisper-activejob), or
130
+ [wisper-que](https://github.com/joevandyk/wisper-que).
131
+
132
+ Depending on the adapter used the listener may need to be a class instead of an object. In this situation, every method corresponding to events should be declared as a class method, too. For example:
133
+
134
+ ```ruby
135
+ class OrderNotifier
136
+ # declare a class method if you are subscribing the listener class instead of its instance like:
137
+ # cancel_order.subscribe(OrderNotifier)
138
+ #
139
+ def self.cancel_order_successful(order_id)
140
+ order = Order.find_by_id(order_id)
141
+
142
+ # notify someone ...
143
+ end
144
+ end
145
+ ```
146
+
147
+ ### ActionController
148
+
149
+ ```ruby
150
+ class CancelOrderController < ApplicationController
151
+
152
+ def create
153
+ cancel_order = CancelOrder.new
154
+
155
+ cancel_order.subscribe(OrderMailer, async: true)
156
+ cancel_order.subscribe(ActivityRecorder, async: true)
157
+ cancel_order.subscribe(StatisticsRecorder, async: true)
158
+
159
+ cancel_order.on(:cancel_order_successful) { |order_id| redirect_to order_path(order_id) }
160
+ cancel_order.on(:cancel_order_failed) { |order_id| render action: :new }
161
+
162
+ cancel_order.call(order_id)
163
+ end
164
+ end
165
+ ```
166
+
167
+ ### ActiveRecord
168
+
169
+ If you wish to publish directly from ActiveRecord models you can broadcast events from callbacks:
170
+
171
+ ```ruby
172
+ class Order < ActiveRecord::Base
173
+ include Wisper::Publisher
174
+
175
+ after_commit :publish_creation_successful, on: :create
176
+ after_validation :publish_creation_failed, on: :create
177
+
178
+ private
179
+
180
+ def publish_creation_successful
181
+ broadcast(:order_creation_successful, self)
182
+ end
183
+
184
+ def publish_creation_failed
185
+ broadcast(:order_creation_failed, self) if errors.any?
186
+ end
187
+ end
188
+ ```
189
+
190
+ There are more examples in the [Wiki](https://github.com/krisleech/wisper/wiki).
191
+
192
+ ## Global Listeners
193
+
194
+ Global listeners receive all broadcast events which they can respond to.
195
+
196
+ This is useful for cross cutting concerns such as recording statistics, indexing, caching and logging.
197
+
198
+ ```ruby
199
+ Wisper.subscribe(MyListener.new)
200
+ ```
201
+
202
+ In a Rails app you might want to add your global listeners in an initalizer.
203
+
204
+ Global listeners are threadsafe.
205
+
206
+ ### Scoping by publisher class
207
+
208
+ You might want to globally subscribe a listener to publishers with a certain
209
+ class.
210
+
211
+ ```ruby
212
+ Wisper.subscribe(MyListener.new, scope: :MyPublisher)
213
+ Wisper.subscribe(MyListener.new, scope: MyPublisher)
214
+ Wisper.subscribe(MyListener.new, scope: "MyPublisher")
215
+ Wisper.subscribe(MyListener.new, scope: [:MyPublisher, :MyOtherPublisher])
216
+ ```
217
+
218
+ This will subscribe the listener to all instances of the specified class(es) and their
219
+ subclasses.
220
+
221
+ Alternatively you can also do exactly the same with a publisher class itself:
222
+
223
+ ```ruby
224
+ MyPublisher.subscribe(MyListener.new)
225
+ ```
226
+
227
+ ## Temporary Global Listeners
228
+
229
+ You can also globally subscribe listeners for the duration of a block.
230
+
231
+ ```ruby
232
+ Wisper.subscribe(MyListener.new, OtherListener.new) do
233
+ # do stuff
234
+ end
235
+ ```
236
+
237
+ Any events broadcast within the block by any publisher will be sent to the
238
+ listeners.
239
+
240
+ This is useful for capturing events published by objects to which you do not have access in a given context.
241
+
242
+ Temporary Global Listeners are threadsafe.
243
+
244
+ ## Subscribing to selected events
245
+
246
+ By default a listener will get notified of all events it can respond to. You
247
+ can limit which events a listener is notified of by passing a string, symbol,
248
+ array or regular expression to `on`:
249
+
250
+ ```ruby
251
+ post_creator.subscribe(PusherListener.new, on: :create_post_successful)
252
+ ```
253
+
254
+ ## Prefixing broadcast events
255
+
256
+ If you would prefer listeners to receive events with a prefix, for example
257
+ `on`, you can do so by passing a string or symbol to `prefix:`.
258
+
259
+ ```ruby
260
+ post_creator.subscribe(PusherListener.new, prefix: :on)
261
+ ```
262
+
263
+ If `post_creator` were to broadcast the event `post_created` the subscribed
264
+ listeners would receive `on_post_created`. You can also pass `true` which will
265
+ use the default prefix, "on".
266
+
267
+ ## Mapping an event to a different method
268
+
269
+ By default the method called on the listener is the same as the event
270
+ broadcast. However it can be mapped to a different method using `with:`.
271
+
272
+ ```ruby
273
+ report_creator.subscribe(MailResponder.new, with: :successful)
274
+ ```
275
+
276
+ This is pretty useless unless used in conjunction with `on:`, since all events
277
+ will get mapped to `:successful`. Instead you might do something like this:
278
+
279
+ ```ruby
280
+ report_creator.subscribe(MailResponder.new, on: :create_report_successful,
281
+ with: :successful)
282
+ ```
283
+
284
+ If you pass an array of events to `on:` each event will be mapped to the same
285
+ method when `with:` is specified. If you need to listen for select events
286
+ _and_ map each one to a different method subscribe the listener once for
287
+ each mapping:
288
+
289
+ ```ruby
290
+ report_creator.subscribe(MailResponder.new, on: :create_report_successful,
291
+ with: :successful)
292
+
293
+ report_creator.subscribe(MailResponder.new, on: :create_report_failed,
294
+ with: :failed)
295
+ ```
296
+
297
+ You could also alias the method within your listener, as such
298
+ `alias successful create_report_successful`.
299
+
300
+ ## Testing
301
+
302
+ Testing matchers and stubs are in seperate gems.
303
+
304
+ * [wisper-rspec](https://github.com/krisleech/wisper-rspec)
305
+ * [wisper-minitest](https://github.com/digitalcuisine/wisper-minitest)
306
+
307
+ ### Clearing Global Listeners
308
+
309
+ If you use global listeners in non-feature tests you _might_ want to clear them
310
+ in a hook to prevent global subscriptions persisting between tests.
311
+
312
+ ```ruby
313
+ after { Wisper.clear }
314
+ ```
315
+
316
+ ## Need help?
317
+
318
+ The [Wiki](https://github.com/krisleech/wisper/wiki) has more examples,
319
+ articles and talks.
320
+
321
+ Got a specific question, try the
322
+ [Wisper tag on StackOverflow](http://stackoverflow.com/questions/tagged/wisper).
323
+
324
+ ## Compatibility
325
+
326
+ Tested with MRI 2.x, JRuby and Rubinius.
327
+
328
+ See the [build status](https://travis-ci.org/krisleech/wisper) for details.
329
+
330
+ ## Running Specs
331
+
332
+ ```
333
+ bundle exec rspec
334
+ ```
335
+
336
+ To run the specs on code changes try [entr](http://entrproject.org/):
337
+
338
+ ```
339
+ ls **/*.rb | entr bundle exec rspec
340
+ ```
341
+
342
+ ## Contributing
343
+
344
+ Please read the [Contributing Guidelines](https://github.com/krisleech/wisper/blob/master/CONTRIBUTING.md).
345
+
346
+ ## Security
347
+
348
+ * gem releases are [signed](http://guides.rubygems.org/security/) ([public key](https://github.com/krisleech/wisper/blob/master/gem-public_cert.pem))
349
+ * commits are GPG signed ([public key](https://pgp.mit.edu/pks/lookup?op=get&search=0x3ABC74851F7CCC88))
350
+
351
+ ## License
352
+
353
+ (The MIT License)
354
+
355
+ Copyright (c) 2013 Kris Leech
356
+
357
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
358
+ this software and associated documentation files (the 'Software'), to deal in
359
+ the Software without restriction, including without limitation the rights to
360
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
361
+ of the Software, and to permit persons to whom the Software is furnished to do
362
+ so, subject to the following conditions:
363
+
364
+ The above copyright notice and this permission notice shall be included in all
365
+ copies or substantial portions of the Software.
366
+
367
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
368
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
369
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
370
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
371
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
372
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
373
+ SOFTWARE.