emittance 0.0.1 → 0.0.2

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 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