emittance 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 000a59df54901139fde2f395a810c5f8a7baf8fd
4
- data.tar.gz: c23462f13256fbc860bd5bf56ae244f49e09d2bf
3
+ metadata.gz: 480d65a814b24509517a96de13d0590117504f2d
4
+ data.tar.gz: 3c41c536af288cb32adc9cc00f2be9c0f8c7f15c
5
5
  SHA512:
6
- metadata.gz: eafdc2ad4b68f42a04f075aefee0a3359bea3c765b58130d33571e9bf1e802b124d6151e6ac183922cce24b2d84ad780fb0b2c7f94ab93d2adb9245d4fb47cea
7
- data.tar.gz: cdc43830eb7f439764fb8e284bcf6c87bf415c69d09f1195282c4297f018cbe1dacab6c52fc2e2aa1fdb5ae46f14b35d3889c156b0b5afd7cf78952042ca47e9
6
+ metadata.gz: d1946bdae18b76de6989aef2342a8ed87341a9d0475dfc1f5459598bdd804c69a97c789f6bbb37af270341d1a8cbda2d2249e0f1adada2a74066883fcf5b5729
7
+ data.tar.gz: 2b1735b0f9f56884ae2e9520b48df74b7540f26565c125a3272ec9165614cbebfa8b3ee2e36a64918b3c244589ff71a6100daeb667947abbe2fb25fc3fae3739
data/.gitignore CHANGED
@@ -15,3 +15,6 @@ tmp/**
15
15
  .yardoc/**
16
16
  coverage/**
17
17
  doc/**
18
+
19
+ # Other
20
+ .rspec_status
data/.rubocop.yml ADDED
@@ -0,0 +1,12 @@
1
+ # Metrics
2
+
3
+ Metrics/LineLength:
4
+ Max: 120
5
+
6
+ # Layout
7
+
8
+ Layout/MultilineMethodCallIndentation:
9
+ EnforcedStyle: indented
10
+
11
+ Layout/MultilineOperationIndentation:
12
+ EnforcedStyle: indented
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.2
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at tyguillen@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Tyler Guillen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # Emittance
2
2
 
3
+ [![Build Status](https://travis-ci.org/aastronautss/emittance.svg?branch=master)](https://travis-ci.org/aastronautss/emittance)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/b5900e32c5a385c96c95/maintainability)](https://codeclimate.com/github/aastronautss/emittance/maintainability)
5
+
3
6
  Emittance is a flexible eventing library that provides a clean interface for both emitting and capturing events. It follows the following workflow:
4
7
 
5
8
  1. Objects (and therefore, classes) can emit events, identified by a symbol.
6
- 2. Events are objects that know who emitted them. Their
9
+ 2. Events are objects that know who emitted them, what time the event was emitted, and any additional information.
7
10
  3. Objects (and therefore, classes) can watch for events that get emitted.
8
11
 
9
12
  Per this pattern, objects are responsible for knowing what events they want to listen to. While this is pragmatically the same as a "push"-style message system (watchers don't need to go check a topic themselves), the semantics are a little different.
@@ -28,7 +31,92 @@ Or install it yourself as:
28
31
 
29
32
  ## Usage
30
33
 
31
- Coming soon!
34
+ If you want a class and its instances to be able to emit events, have it extend `Emittance::Emitter`.
35
+
36
+ ```ruby
37
+ class Foo
38
+ extend Emittance::Emitter
39
+ end
40
+ ```
41
+
42
+ Emitters can emit events like so:
43
+
44
+ ```ruby
45
+ my_foo = Foo.new
46
+ my_foo.emit :something_happened
47
+ Foo.emit :something_else_happened # Classes who extended Emitter can also emit events!
48
+ ```
49
+
50
+ As you can see, event types are identified by a symbol. More on that later. You can also pass in an optional payload, which can be any object:
51
+
52
+ ```ruby
53
+ my_foo.emit :something_happened, "Here's a payload!"
54
+ ```
55
+
56
+ The above examples are cool, but it's generally a better idea to have an object emit its own events:
57
+
58
+ ```ruby
59
+ class Foo
60
+ extend Emittance::Emitter
61
+
62
+ def make_something_happen
63
+ emit :something_happened, "Here's a payload!"
64
+ end
65
+ end
66
+
67
+ my_foo = Foo.new
68
+ my_foo.make_something_happen
69
+ ```
70
+
71
+ What happens with these events? Watchers are objects that capture these event emissions. You can set up a watcher by including or extending `Emittance::Watcher`:
72
+
73
+ ```ruby
74
+ class Bar
75
+ extend Emittance::Watcher
76
+ end
77
+ ```
78
+
79
+ To watch for these events, you can just call the `watch` method, which takes the symbol identifier and a block that serves as a callback:
80
+
81
+ ```ruby
82
+ Bar.watch :something_happened do |event|
83
+ puts 'Something definitely happened!'
84
+ puts event.identifier.inspect
85
+ puts event.payload
86
+ end
87
+
88
+ my_foo.make_something_happen
89
+ # prints:
90
+ # Something definitely happened!
91
+ # :something_happened
92
+ # Here's a payload!
93
+ ```
94
+
95
+ Note that the block gets passed an "event" object, which has some attributes. See the docs for more details.
96
+
97
+ You can also make `watch` call a method:
98
+
99
+ ```ruby
100
+ class Bar
101
+ extend Emittance::Watcher
102
+
103
+ def self.greet(event)
104
+ puts 'Hello, something must have happened!'
105
+ puts event.identifier.inspect
106
+ puts event.payload
107
+ end
108
+ end
109
+
110
+ Bar.watch :something_happened, :greet
111
+
112
+ my_foo.make_something_happen
113
+ # prints:
114
+ # Hello, something must have happened!
115
+ # :something_happened
116
+ # Here's a payload!
117
+ ```
118
+
119
+ Those are the basics--for more info, check the docs!
32
120
 
33
121
  ## Development
34
122
 
@@ -38,4 +126,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
38
126
 
39
127
  ## Contributing
40
128
 
41
- Bug reports and pull requests are welcome on GitHub at https://github.com/aastronauts/emittance.
129
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aastronautss/emittance.
@@ -1,199 +1,192 @@
1
- ##
2
- # There are certain classes (ergo objects) that represent an action taken by another object. This pattern goes like so:
3
- #
4
- # class Foo
5
- # def assign
6
- # Assignment.new(self).call
7
- # end
8
- # end
9
- #
10
- # class Assignment
11
- # attr_reader :assignable
12
- #
13
- # def initialize(assignable)
14
- # @assignable = assignable
15
- # end
16
- #
17
- # def call
18
- # do_stuff
19
- # end
20
- #
21
- # # ...
22
- # end
23
- #
24
- # This pattern is useful for maintaining the single responsibility principle, delegating complex tasks to other objects
25
- # even when (in this particular case), it might be sensible for the +assign+ message to be sent to +Foo+. This has
26
- # numerous benefits, including the ability for actions like +Assignment+ to take a duck type.
27
- #
28
- # However, this can easily just become a proxy for the same antipattern it was made to solve. We might wind up with a
29
- # +#call+ method like the following:
30
- #
31
- # class Assignment
32
- # # ...
33
- #
34
- # def call
35
- # do_stuff
36
- # do_stuff_to_another_object
37
- # do_stuff_to_something_else
38
- # do_stuff_to_yet_another_thing
39
- # end
40
- #
41
- # # ...
42
- # end
43
- #
44
- # +Assignment+ is suddenly collaborating with a whole bunch of objects! This isn't bad in itself, but it might cause
45
- # some problems further on down the road as we add more responsibilities to +Assignment+. Do we really want all these
46
- # things to happen every time an +Assignment+ happens? If not, assuming this pattern we'll have to add a bunch of
47
- # control flow:
48
- #
49
- # class Assignment
50
- # # ...
51
- #
52
- # def call
53
- # do_stuff
54
- #
55
- # if some_condition
56
- # do_stuff_to_another_object
57
- #
58
- # if some_other_condition
59
- # do_stuff_to_something_else
60
- # else
61
- # do_stuff_to_yet_another_thing
62
- # end
63
- # elsif yet_another_condition
64
- # do_other_stuff_to_that_other_object
65
- # else
66
- # dont_actually_do_anything_but_notify_someone
67
- # end
68
- # end
69
- #
70
- # # ...
71
- # end
72
- #
73
- # This is obviously an extreme example (but not unheard of!), but it gets to the core of what this module tries to
74
- # solve. +Emittance::Action+ helps facilitate the single responsibility principle by emitting an event whenever we
75
- # invoke +#call+ on an object like +Assignment+.
76
- #
77
- # == Usage
78
- #
79
- # First, define a class and include this module:
80
- #
81
- # class Assignment
82
- # include Emittance::Action
83
- #
84
- # attr_reader :assignable
85
- #
86
- # def initialize(assignable)
87
- # @assignable = assignable
88
- # end
89
- # end
90
- #
91
- # Per the pattern explained above, instances of this class are representations of an action being carried out. This
92
- # class should have a very minimal interface (maybe, at most, some getter methods for its instance variables so the
93
- # handler can make decisions based on its state).
94
- #
95
- # Next, we'll implement the +#call+ instance method. +Emittance::Action+ will take care of the dirty work for us:
96
- #
97
- # class Assignment
98
- # # ...
99
- #
100
- # def call
101
- # do_one_and_i_mean_only_one_thing
102
- # end
103
- #
104
- # # ...
105
- # end
106
- #
107
- # Again, this method should do a single thing. From here, your code should be able to run without error! You might
108
- # notice, though, that a mysterious class will have been defined after loading this file.
109
- #
110
- # defined? AssignmentHandler
111
- # => "constant"
112
- #
113
- # Next, we can open up this class to implement the event handler. +Emittance+ will look for a method called
114
- # +#handle_call+, and invoke it whenever, in this example, +Assignment#call+ is called.
115
- #
116
- # class AssignmentHandler
117
- # def handle_call
118
- # notify_someone(action)
119
- # end
120
- #
121
- # # ...
122
- # end
123
- #
124
- # The "Action" object is stored as the instance variable +@action+, made available with a getter class +#action+. This
125
- # will allow us to access its data and make decisions based on it.
126
- #
127
- # Now, this seems like we're passing the buck of all that control flow to yet another object, but this pattern has
128
- # several advantages. First, we can disable +Emittance+ at will, so if we ever want to shut +Assignment+ actions
129
- # off from their listeners, that is always an option to us. Second, to address the concern raised at the beginning of
130
- # this paragraph, this paradigm puts us into the mindset of spreading the flow of our program out across multiple
131
- # action/handler pairs, allowing us to think more clearly about what our code is doing.
132
- #
133
- # One possible disadvantage of this pattern is that it suggests a one-to-one pairing between events and handlers.
134
- #
135
- module Emittance::Action
136
- EMITTING_METHOD = :call
137
- HANDLER_METHOD_NAME = "handle_#{EMITTING_METHOD}".to_sym
138
-
139
- class NoHandlerMethodError < StandardError; end
140
-
141
- # @private
142
- class << self
143
- def included(action_klass)
144
- handler_klass_name = Emittance::Action.handler_klass_name(action_klass)
145
- handler_klass = Emittance::Action.find_or_create_klass(handler_klass_name)
146
-
147
- action_klass.class_eval do
148
- extend Emittance::Emitter
149
-
150
- class << self
151
- # @private
152
- def method_added(method_name)
153
- emitting_method = Emittance::Action::EMITTING_METHOD
154
- emits_on method_name if method_name == emitting_method
155
- super
1
+ # frozen_string_literal: true
2
+
3
+ module Emittance
4
+ ##
5
+ # Consider the usual "Service Object" pattern:
6
+ #
7
+ # class Foo
8
+ # def assign
9
+ # FooAssignment.new(self).call
10
+ # end
11
+ # end
12
+ #
13
+ # class FooAssignment
14
+ # attr_reader :assignable
15
+ #
16
+ # def initialize(assignable)
17
+ # @assignable = assignable
18
+ # end
19
+ #
20
+ # def call
21
+ # do_stuff
22
+ # end
23
+ #
24
+ # # ...
25
+ # end
26
+ #
27
+ # There are variations on this pattern, the idea is that the service object represents something that your application
28
+ # is doing. However, this can easily just become a proxy for the same antipattern it was made to solve. We might wind
29
+ # up with a +#call+ method like the following:
30
+ #
31
+ # class FooAssignment
32
+ # # ...
33
+ #
34
+ # def call
35
+ # do_stuff
36
+ # do_stuff_to_another_object
37
+ # do_stuff_to_something_else
38
+ # do_stuff_to_yet_another_thing
39
+ # end
40
+ #
41
+ # # ...
42
+ # end
43
+ #
44
+ # We can use the +Emittance+ core features to prune those method calls:
45
+ #
46
+ # class FooAssignment
47
+ # extend Emittance::Emitter
48
+ #
49
+ # # ...
50
+ #
51
+ # def call
52
+ # do_stuff
53
+ # emit :foo_assignment
54
+ # end
55
+ #
56
+ # # ...
57
+ # end
58
+ #
59
+ # +Emittance::Action+ provides a shortcut for this. Just mix it in and implement +#call+! This allows us to keep the
60
+ # expressitivity that a Service Object is made to provide, while preventing us from having to give such an object too
61
+ # many responsibilities.
62
+ #
63
+ # == Usage
64
+ #
65
+ # First, define a class and include this module:
66
+ #
67
+ # class FooAssignment
68
+ # include Emittance::Action
69
+ #
70
+ # attr_reader :assignable
71
+ #
72
+ # def initialize(assignable)
73
+ # @assignable = assignable
74
+ # end
75
+ # end
76
+ #
77
+ # Next, we'll implement the +#call+ instance method. +Emittance::Action+ will take care of the dirty work for us:
78
+ #
79
+ # class FooAssignment
80
+ # # ...
81
+ #
82
+ # def call
83
+ # do_one_and_i_mean_only_one_thing
84
+ # end
85
+ #
86
+ # # ...
87
+ # end
88
+ #
89
+ # From here, your code should be able to run without error! You might notice, though, that a mysterious class will
90
+ # have been defined after loading this file.
91
+ #
92
+ # defined? FooAssignmentHandler
93
+ # => "constant"
94
+ #
95
+ # Next, we can open up this class to implement the event handler. +Emittance+ will look for a method called
96
+ # +#handle_call+, and invoke it whenever, in this example, +FooAssignment#call+ is called.
97
+ #
98
+ # class FooAssignmentHandler
99
+ # def handle_call
100
+ # notify_someone(action)
101
+ # end
102
+ #
103
+ # # ...
104
+ # end
105
+ #
106
+ # The "Action" object is stored as the instance variable +@action+, made available with a getter class +#action+. This
107
+ # will allow us to access its data and make decisions based on it.
108
+ #
109
+ # Now, this seems like we're passing the buck of all that control flow to yet another object, but this pattern has
110
+ # several advantages. First, we can disable +Emittance+ at will, so if we ever want to shut +FooAssignment+ actions
111
+ # off from their listeners, that is always an option to us. Second, to address the concern raised at the beginning of
112
+ # this paragraph, this paradigm puts us into the mindset of spreading the flow of our program out across multiple
113
+ # action/handler pairs, allowing us to think more clearly about what our code is doing.
114
+ #
115
+ # One possible disadvantage of this pattern is that it suggests a one-to-one pairing between events and handlers.
116
+ #
117
+ module Action
118
+ # Name of the method that will emit an event when invoked.
119
+ EMITTING_METHOD = :call
120
+ # Name of the method that will be invoked when the handler class captures an event.
121
+ HANDLER_METHOD_NAME = "handle_#{EMITTING_METHOD}".to_sym
122
+
123
+ # @private
124
+ class << self
125
+ def included(action_klass)
126
+ handler_klass_name = Emittance::Action.handler_klass_name(action_klass)
127
+ handler_klass = Emittance::Action.find_or_create_klass(handler_klass_name)
128
+
129
+ action_klass.class_eval do
130
+ extend Emittance::Emitter
131
+
132
+ class << self
133
+ # @private
134
+ def method_added(method_name)
135
+ emitting_method = Emittance::Action::EMITTING_METHOD
136
+ emits_on method_name if method_name == emitting_method
137
+ super
138
+ end
156
139
  end
157
140
  end
158
- end
159
141
 
160
- handler_klass.class_eval do
161
- attr_reader :action
142
+ handler_klass.class_eval do
143
+ attr_reader :action
162
144
 
163
- extend Emittance::Watcher
145
+ extend Emittance::Watcher
164
146
 
165
- def initialize(action_obj)
166
- @action = action_obj
167
- end
147
+ def initialize(action_obj)
148
+ @action = action_obj
149
+ end
168
150
 
169
- watch Emittance::Action.emitting_event_name(action_klass) do |event|
170
- handler_obj = new(event.emitter)
171
- handler_method_name = Emittance::Action::HANDLER_METHOD_NAME
151
+ watch Emittance::Action.emitting_event_name(action_klass) do |event|
152
+ handler_obj = new(event.emitter)
153
+ handler_method_name = Emittance::Action::HANDLER_METHOD_NAME
172
154
 
173
- if handler_obj.respond_to? handler_method_name
174
- handler_obj.send handler_method_name
155
+ if handler_obj.respond_to? handler_method_name
156
+ handler_obj.send handler_method_name
157
+ end
175
158
  end
176
159
  end
177
160
  end
178
- end
179
161
 
180
- # @private
181
- def handler_klass_name(action_klass)
182
- "#{action_klass}Handler"
183
- end
162
+ # @private
163
+ def handler_klass_name(action_klass)
164
+ "#{action_klass}Handler"
165
+ end
184
166
 
185
- # @private
186
- def emitting_event_name(action_klass)
187
- Emittance::Emitter.emitting_method_event(action_klass, Emittance::Action::EMITTING_METHOD)
188
- end
167
+ # @private
168
+ def emitting_event_name(action_klass)
169
+ Emittance::Emitter.emitting_method_event(action_klass, Emittance::Action::EMITTING_METHOD)
170
+ end
189
171
 
190
- # @private
191
- def find_or_create_klass(klass_name)
192
- unless Object.const_defined? klass_name
193
- Object.const_set klass_name, Class.new(Object)
172
+ # @private
173
+ def find_or_create_klass(klass_name)
174
+ unless Object.const_defined? klass_name
175
+ set_namespaced_constant_by_name klass_name, Class.new
176
+ end
177
+
178
+ Object.const_get klass_name
194
179
  end
195
180
 
196
- Object.const_get klass_name
181
+ private
182
+
183
+ def set_namespaced_constant_by_name(const_name, obj)
184
+ names = const_name.split('::')
185
+ names.shift if names.size > 1 && names.first.empty?
186
+
187
+ namespace = names.size == 1 ? Object : Object.const_get(names[0...-1].join('::'))
188
+ namespace.const_set names.last, obj
189
+ end
197
190
  end
198
191
  end
199
192
  end