shout 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +8 -0
- data/examples/snowcone.rb +74 -0
- data/lib/shout.rb +23 -0
- data/lib/shout/listener.rb +70 -0
- data/lib/shout/observable.rb +89 -0
- data/lib/shout/version.rb +3 -0
- data/shout.gemspec +24 -0
- data/test/shout_test.rb +24 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a91e27be479d63ef940f9f61631c9249731b23f5
|
4
|
+
data.tar.gz: 0b155a9e8cc93e3631b6b71f6558ad5a985892b7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8850dbe034e3ae92442a514c495e5b10f082be4832b24f5827dcd64c4774bf8cd6c184d63567bfec87ec62bbfce6290d7f8e3eb41cd339c57e0c4f3642a1ac23
|
7
|
+
data.tar.gz: 8dac0aea72845cdc2e84b368ba5f86bc24a23421ccb4066d5a63ea0153ee4967122ec155c0b86b4ea3d72c6c8a5f00008a6adfc62783b041dc1755ff03fd2d67
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Tim Snowhite
|
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,93 @@
|
|
1
|
+
# Shout
|
2
|
+
|
3
|
+
A class-level observer pattern for ActiveRecord.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'shout'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install shout
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Shout provides two mixins: Observable, and Listener.
|
23
|
+
|
24
|
+
|
25
|
+
## Observable
|
26
|
+
|
27
|
+
``` ruby
|
28
|
+
class Snowcone < Struct.new(:state, :flavors)
|
29
|
+
|
30
|
+
# To publish events, include the Observable module.
|
31
|
+
include Shout::Observable
|
32
|
+
|
33
|
+
# List the events you'll publish. This cuts down on typos in the
|
34
|
+
# observing classes.
|
35
|
+
shout_events([
|
36
|
+
:empty,
|
37
|
+
:filled,
|
38
|
+
:flavored,
|
39
|
+
:purchased,
|
40
|
+
:eaten,
|
41
|
+
:thrown_away,
|
42
|
+
])
|
43
|
+
# List your observing classes. This ensures that callbacks run in
|
44
|
+
# a deterministic order across all environments.
|
45
|
+
shout_observers([:Accounting])
|
46
|
+
|
47
|
+
# Then in your instances, use the #run_callbacks method
|
48
|
+
# to notify Listeners that an event has occurred.
|
49
|
+
def state!(event)
|
50
|
+
self.run_shout_callbacks(event)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
## Listener
|
56
|
+
|
57
|
+
``` ruby
|
58
|
+
class Accounting
|
59
|
+
|
60
|
+
# To listen to events on other objects, include Listener.
|
61
|
+
include Shout::Listener
|
62
|
+
|
63
|
+
# And reference the class name you'll be observing.
|
64
|
+
# You can only observe one class.
|
65
|
+
self.observes= :Snowcone
|
66
|
+
|
67
|
+
# Implement the initialize method with an argument for the
|
68
|
+
# observed instance.
|
69
|
+
def initialize(snowcone)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set callbacks for the events you'd like to listen to.
|
73
|
+
shout_callback :purchased, :credit_bank_account
|
74
|
+
shout_callback :flavored, :mark_flavor_used
|
75
|
+
shout_callback :filled, :mark_ice_used
|
76
|
+
|
77
|
+
# And point them at methods in your class.
|
78
|
+
def credit_bank_account(cash)
|
79
|
+
end
|
80
|
+
def mark_flavor_used(flavor)
|
81
|
+
end
|
82
|
+
def mark_ice_used
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
## Contributing
|
88
|
+
|
89
|
+
1. Fork it
|
90
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
91
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
92
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
93
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'shout'
|
2
|
+
BANK_ACCOUNT = {
|
3
|
+
:credits => [],
|
4
|
+
}
|
5
|
+
INVENTORY = {
|
6
|
+
:banana_syrup => 1,
|
7
|
+
:blue_raspberry_syrup => 3,
|
8
|
+
:bubblegum_syrup => 5,
|
9
|
+
:ice => 1000,
|
10
|
+
}
|
11
|
+
|
12
|
+
class Accounting
|
13
|
+
def initialize(snowcone)
|
14
|
+
@snowcone = snowcone
|
15
|
+
end
|
16
|
+
def credit_bank_account(cash)
|
17
|
+
BANK_ACCOUNT[:credits].push(cash)
|
18
|
+
end
|
19
|
+
def mark_flavor_used(flavor)
|
20
|
+
INVENTORY[:"#{flavor}_syrup"] -= 1
|
21
|
+
end
|
22
|
+
def mark_ice_used
|
23
|
+
INVENTORY[:ice] -= 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Snowcone < Struct.new(:state, :flavors)
|
28
|
+
include Shout::Observable
|
29
|
+
shout_events([
|
30
|
+
:empty,
|
31
|
+
:filled,
|
32
|
+
:flavored,
|
33
|
+
:purchased,
|
34
|
+
:eaten,
|
35
|
+
:thrown_away,
|
36
|
+
])
|
37
|
+
shout_observers([
|
38
|
+
:Accounting,
|
39
|
+
])
|
40
|
+
def initialize
|
41
|
+
@state = :empty
|
42
|
+
@flavors = []
|
43
|
+
end
|
44
|
+
def flavor(flavor)
|
45
|
+
@flavors.push(flavor)
|
46
|
+
self.run_shout_callbacks(:flavored, flavor)
|
47
|
+
end
|
48
|
+
def state!(event)
|
49
|
+
self.run_shout_callbacks(event)
|
50
|
+
self.state = event
|
51
|
+
end
|
52
|
+
def purchase(cost)
|
53
|
+
self.run_shout_callbacks(:purchased, cost)
|
54
|
+
self.state= :purchased
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Accounting
|
59
|
+
include Shout::Listener
|
60
|
+
self.observes= :Snowcone
|
61
|
+
|
62
|
+
shout_callback :purchased, :credit_bank_account
|
63
|
+
shout_callback :flavored, :mark_flavor_used
|
64
|
+
shout_callback :filled, :mark_ice_used
|
65
|
+
end
|
66
|
+
|
67
|
+
class Barista
|
68
|
+
def self.order(*flavors)
|
69
|
+
s=Snowcone.new
|
70
|
+
s.state!(:filled)
|
71
|
+
flavors.map{|i| s.flavor(i)}
|
72
|
+
s
|
73
|
+
end
|
74
|
+
end
|
data/lib/shout.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "shout/version"
|
2
|
+
require 'shout/observable'
|
3
|
+
require 'shout/listener'
|
4
|
+
module Shout
|
5
|
+
module_function
|
6
|
+
def constantize(camel_cased_word)
|
7
|
+
names = camel_cased_word.split('::')
|
8
|
+
|
9
|
+
# Trigger a builtin NameError exception including the ill-formed constant in the message.
|
10
|
+
Object.const_get(camel_cased_word) if names.empty?
|
11
|
+
|
12
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
13
|
+
names.shift if names.size > 1 && names.first.empty?
|
14
|
+
|
15
|
+
names.inject(Object) do |constant, name|
|
16
|
+
if constant == Object
|
17
|
+
constant.const_get(name)
|
18
|
+
else
|
19
|
+
candidate = constant.const_get(name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Shout
|
2
|
+
|
3
|
+
module Listener #include me
|
4
|
+
|
5
|
+
## Implementor Methods
|
6
|
+
def initialize(observed)
|
7
|
+
raise ArgumentError.new("Must be overriden in implementors.")
|
8
|
+
end
|
9
|
+
|
10
|
+
## Internal Methods
|
11
|
+
def self.included(mod)
|
12
|
+
mod.extend(ClassMethods)
|
13
|
+
mod.callbacks = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_with_shout_event(name, *params)
|
17
|
+
a=self.class.callbacks_for_event(name)
|
18
|
+
a.map{|meth| self.send(meth,*params) }
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
## Implementor Methods
|
23
|
+
|
24
|
+
# Sets the class which this observer will observe. A required call.
|
25
|
+
def observes=(observes)
|
26
|
+
@observes = observes
|
27
|
+
valid_observable?
|
28
|
+
observes_class.observer_class_names_check.push(self.name.to_sym)
|
29
|
+
end
|
30
|
+
# Registers a callback which will be executed when the
|
31
|
+
# observes_class's instances call run_callbacks(name).
|
32
|
+
def shout_callback(name, method)
|
33
|
+
valid_event?(name)
|
34
|
+
callbacks.push([name,method])
|
35
|
+
end
|
36
|
+
def test_event(name, instance, *params)
|
37
|
+
listener = new(instance)
|
38
|
+
listener.update_with_shout_event(name, *params)
|
39
|
+
end
|
40
|
+
|
41
|
+
## Internal Methods
|
42
|
+
attr_reader :observes
|
43
|
+
attr_accessor :callbacks
|
44
|
+
def observes_class
|
45
|
+
Shout.constantize(self.observes.to_s)
|
46
|
+
rescue NameError => e
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
def valid_observable?
|
50
|
+
unless observes_class
|
51
|
+
raise ArgumentError.new("#{self}.observes = #{observes.inspect}. #{observes.inspect} can't be found as a constant.")
|
52
|
+
end
|
53
|
+
unless observes_class < ::Shout::Observable
|
54
|
+
raise ArgumentError.new("#{self}.observes == #{self.observes} does not inherit from ::Shout::Observable.")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
def valid_event?(event)
|
58
|
+
if observes_class.events_notified_misspellings.has_key?(event)
|
59
|
+
raise ArgumentError.new("#{self.class}: Well well well, you've found an area of cognitive dissonance! Use #{observes_class.events_notified_misspellings[event].inspect} instead of #{event}.")
|
60
|
+
end
|
61
|
+
unless observes_class.events_notified.include?(event)
|
62
|
+
raise ArgumentError.new("#{self.class}: Oh lordy, this is embarassing. #{event.inspect} is totally not available in #{observes_class}.\nAvailable events:\n#{observes_class.events_notified.inspect}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
def callbacks_for_event(n)
|
66
|
+
self.callbacks.select{|name,method| name == n}.map{|name,method| method}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'observer' # stdlib
|
2
|
+
module Shout
|
3
|
+
module Observable #include me
|
4
|
+
|
5
|
+
## ImplementorMethods
|
6
|
+
|
7
|
+
# Really this is for clarity until everyone is cool with the implementation.
|
8
|
+
def self.run_callbacks(instance,event,*params)
|
9
|
+
instance.run_shout_callbacks(event,*params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_shout_callbacks(event, *params)
|
13
|
+
# HMMM: not sure how I feel
|
14
|
+
self.class.load_observers(self) # about this. See #idemp_observer.
|
15
|
+
self.notify_shout_observers(event, *params)
|
16
|
+
end
|
17
|
+
def notify_shout_observers(event, *params)
|
18
|
+
self.shout_observer_list.each do |k,i|
|
19
|
+
i.update_with_shout_event(event, *params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
## Internal Methods
|
24
|
+
def self.included(mod)
|
25
|
+
mod.send(:extend, ClassMethods)
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :shout_observer_list
|
29
|
+
|
30
|
+
# This is done this way because I want to be able to #new these
|
31
|
+
# up at the time an event comes in, rather than using and
|
32
|
+
# after_initialize callback.
|
33
|
+
def idemp_observer(k,i)
|
34
|
+
self.shout_observer_list ||= []
|
35
|
+
return if self.shout_observer_list.assoc(k)
|
36
|
+
self.shout_observer_list.push([i.class, i])
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods #extend me
|
42
|
+
|
43
|
+
## Implementor Methods
|
44
|
+
def shout_events(event_names)
|
45
|
+
self.events_notified += event_names.map(&:to_sym)
|
46
|
+
end
|
47
|
+
def shout_misspellings(event_misspellings)
|
48
|
+
self.events_notified_misspellings.merge!(event_misspellings)
|
49
|
+
end
|
50
|
+
def shout_observers(listener_classes)
|
51
|
+
self.observer_class_names += listener_classes
|
52
|
+
end
|
53
|
+
|
54
|
+
## Internal Methods
|
55
|
+
attr_accessor :observer_class_names
|
56
|
+
attr_accessor :observer_class_names_check
|
57
|
+
attr_accessor :events_notified
|
58
|
+
attr_accessor :events_notified_misspellings
|
59
|
+
def self.extended(mod)
|
60
|
+
mod.observer_class_names = []
|
61
|
+
mod.observer_class_names_check = []
|
62
|
+
mod.events_notified = []
|
63
|
+
mod.events_notified_misspellings = {}
|
64
|
+
end
|
65
|
+
def check_observer_classes
|
66
|
+
return if @checked
|
67
|
+
here_not_there = (observer_class_names - observer_class_names_check)
|
68
|
+
there_not_here = (observer_class_names_check - observer_class_names)
|
69
|
+
unless here_not_there == there_not_here
|
70
|
+
missing = (here_not_there + there_not_here)
|
71
|
+
|
72
|
+
meths = "#{self}.shout_observers(#{missing.map(&:inspect)}) and "+
|
73
|
+
missing.map{|observer|
|
74
|
+
"#{observer}.observes=#{self.name.to_sym.inspect}"
|
75
|
+
}.join(' and ')
|
76
|
+
|
77
|
+
raise ArgumentError.new("#{self}: Please call both #{meths} to observe #{self}. This is done to ensure determinism in the load order in development, test, and production environments.")
|
78
|
+
end
|
79
|
+
@checked = true
|
80
|
+
end
|
81
|
+
def load_observers(instance)
|
82
|
+
check_observer_classes
|
83
|
+
self.observer_class_names.map{|name|
|
84
|
+
k = Shout.constantize(name.to_s)
|
85
|
+
instance.idemp_observer(k,k.new(instance))
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/shout.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'shout/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "shout"
|
8
|
+
spec.version = Shout::VERSION
|
9
|
+
spec.authors = ["Tim Snowhite"]
|
10
|
+
spec.email = ["tim@snowhitesolutions.com"]
|
11
|
+
spec.description = %q{A class-level observer pattern for ActiveRecord.}
|
12
|
+
spec.summary = %q{Publish and subscribe to events on your models.}
|
13
|
+
spec.homepage = ""
|
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|features)/})
|
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 "minitest"
|
24
|
+
end
|
data/test/shout_test.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
gem 'minitest', '~>5.0'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require File.expand_path('../../examples/snowcone', __FILE__)
|
4
|
+
class ShoutTest < Minitest::Test
|
5
|
+
def test_example
|
6
|
+
snowcone = Barista.order(:blue_raspberry,:banana,:bubblegum)
|
7
|
+
snowcone.purchase(5.00)
|
8
|
+
snowcone.state!(:eaten)
|
9
|
+
snowcone.state!(:thrown_away)
|
10
|
+
assert_equal(
|
11
|
+
[
|
12
|
+
999,
|
13
|
+
0,
|
14
|
+
[5.00],
|
15
|
+
],
|
16
|
+
|
17
|
+
[
|
18
|
+
INVENTORY[:ice],
|
19
|
+
INVENTORY[:banana_syrup],
|
20
|
+
BANK_ACCOUNT[:credits],
|
21
|
+
]
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shout
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tim Snowhite
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-18 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: minitest
|
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
|
+
description: A class-level observer pattern for ActiveRecord.
|
56
|
+
email:
|
57
|
+
- tim@snowhitesolutions.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- .travis.yml
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- examples/snowcone.rb
|
69
|
+
- lib/shout.rb
|
70
|
+
- lib/shout/listener.rb
|
71
|
+
- lib/shout/observable.rb
|
72
|
+
- lib/shout/version.rb
|
73
|
+
- shout.gemspec
|
74
|
+
- test/shout_test.rb
|
75
|
+
homepage: ''
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 2.0.14
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Publish and subscribe to events on your models.
|
99
|
+
test_files:
|
100
|
+
- test/shout_test.rb
|