happenings 0.1.0
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/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +186 -0
- data/Rakefile +1 -0
- data/happenings.gemspec +26 -0
- data/lib/happenings/config.rb +42 -0
- data/lib/happenings/event.rb +107 -0
- data/lib/happenings/version.rb +3 -0
- data/lib/happenings.rb +16 -0
- data/spec/happenings/configuration_spec.rb +31 -0
- data/spec/happenings/event_spec.rb +117 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/event.rb +7 -0
- data/spec/support/reset_password_event.rb +35 -0
- data/spec/support/user.rb +10 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ddc6f910e11bfdc46fc02009443d8d946930daa7
|
4
|
+
data.tar.gz: afdf5f4d4c91fee91648729afac51447a305e157
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6abb8f430a2b00daea4a13802c62207a42baf14f2835b9345be1b19f804f0d75723c63cf63eb83bf2b0302a2a630c14f656807d04f88a2a56b0cc8cc41d4cb5f
|
7
|
+
data.tar.gz: 99ecdb60c6c6d5a644149baf0465076ac15cdfc4d6138d2e00144b56da6547698f5d4c9ca616fc888a8cd02e47e0f68a55d32de02f162bac7aa73d8ab7abd56f
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Desmond Bowe
|
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,186 @@
|
|
1
|
+
# Happenings
|
2
|
+
|
3
|
+
A light framework for building and publishing domain events.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'happenings'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install happenings
|
18
|
+
|
19
|
+
## Basic Usage
|
20
|
+
|
21
|
+
Start by creating a Plain Old Ruby Object for your domain event and including the `Happening::Event` module.
|
22
|
+
You'll want to declare an initialize method that sets up any needed variables. Then, implement
|
23
|
+
a `#strategy` method and add your business logic there. This method will be called when your
|
24
|
+
Happening Event is run.
|
25
|
+
|
26
|
+
```
|
27
|
+
class ResetPasswordEvent
|
28
|
+
include Happenings::Event
|
29
|
+
|
30
|
+
def initialize user, new_password, new_password_confirmation
|
31
|
+
@user = user
|
32
|
+
@new_password = new_password
|
33
|
+
@new_password_confirmation = new_password_confirmation
|
34
|
+
end
|
35
|
+
|
36
|
+
def strategy
|
37
|
+
if @new_password == @new_password_confirmation
|
38
|
+
@user.reset_password! @new_password
|
39
|
+
success! message: 'Password reset successfully'
|
40
|
+
else
|
41
|
+
failure! message: 'Password must match confirmation'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Run the event using the `#run` method as follows:
|
48
|
+
|
49
|
+
```
|
50
|
+
event = ResetPasswordEvent.new(user, 'secret_password', 'secret_password')
|
51
|
+
if event.run!
|
52
|
+
# it worked, do something
|
53
|
+
flash[:notice] = event.message
|
54
|
+
else
|
55
|
+
# event failed for some reason
|
56
|
+
flash[:error] = event.message
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
`#run!` will return Boolean `true` or `false` depending on the outcome of your strategy.
|
61
|
+
`#strategy` must return with `#success!` or `#failure!` or a `Happenings::OutcomeError` will
|
62
|
+
be raised.
|
63
|
+
|
64
|
+
## Success, Failure
|
65
|
+
`#success!` and `#failure!` will set a `succeeded?` attribute and set optional keys for
|
66
|
+
`message` and `reason` attributes. `message` is meant for human-readable messages,
|
67
|
+
such as "Password reset failed", whereas `reason` is designed for machine-sortable
|
68
|
+
filtering, such as "confirmation\_mismatch". A `duration` attribute is also recorded.
|
69
|
+
|
70
|
+
|
71
|
+
## Publishing
|
72
|
+
Happenings makes it easy to disseminate your business events to interested parties, such as
|
73
|
+
your analytics system, cache counters, or background workers. Happenings will swallow the
|
74
|
+
events by default, but it's recommended that you set a publisher in the configuration (see below).
|
75
|
+
This publisher must respond to a `publish` method that accepts two arguments: the
|
76
|
+
payload and a hash of additional info. This arrangement is geared towards a message broker like
|
77
|
+
RabbitMQ, but you can certainly write your own wrapper for another messaging bus like Redis.
|
78
|
+
|
79
|
+
Publishing happens automatically when `#run!` is called, regardless of the strategy outcome. The following methods are important:
|
80
|
+
|
81
|
+
`payload`: The main package of the event. defaults to `{}`, but should
|
82
|
+
be overridden in your event to include useful info such as the user id, changed attributes, etc.
|
83
|
+
|
84
|
+
`routing_key`: The routable description of the event. Defaults to `#{app_name}.#{event_name}.#{outcome}`, where outcome is either 'success' or 'failure'.
|
85
|
+
|
86
|
+
`event_name`: A machine-filterable version of the event. Defaults to the underscored class name.
|
87
|
+
|
88
|
+
Here's an expanded version of our Reset Password example above that includes publishing features:
|
89
|
+
|
90
|
+
```
|
91
|
+
class MyEventPublisher
|
92
|
+
require 'bunny'
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
@rabbitmq = Bunny.new
|
96
|
+
@rabbitmq.start
|
97
|
+
@rabbitmq_channel = @rabbitmq.create_channel
|
98
|
+
@events_exchange = @rabbitmq_channel.topic 'events', durable: true
|
99
|
+
end
|
100
|
+
|
101
|
+
def publish message, properties
|
102
|
+
@events_exchange.publish JSON.dump(message), properties
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
Happenings.configure do |config|
|
108
|
+
config.publisher = MyEventPublisher.new
|
109
|
+
config.app_name = 'my_app'
|
110
|
+
end
|
111
|
+
|
112
|
+
class ResetPasswordEvent
|
113
|
+
include Happenings::Event
|
114
|
+
|
115
|
+
attr_reader :user, :new_password, :new_password_confirmation
|
116
|
+
|
117
|
+
def initialize user, new_password, new_password_confirmation
|
118
|
+
@user = user
|
119
|
+
@new_password = new_password
|
120
|
+
@new_password_confirmation = new_password_confirmation
|
121
|
+
end
|
122
|
+
|
123
|
+
def strategy
|
124
|
+
ensure_passwords_match and
|
125
|
+
reset_user_password
|
126
|
+
end
|
127
|
+
|
128
|
+
def payload
|
129
|
+
{ user: { id: user.id } }
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def reset_user_password
|
136
|
+
user.reset_password! new_password
|
137
|
+
success! message: 'Password reset successfully'
|
138
|
+
end
|
139
|
+
|
140
|
+
def ensure_passwords_match
|
141
|
+
new_password == new_password_confirmation or
|
142
|
+
failure! message: 'Password must match confirmation'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
If the event is successful, `MyEventPublisher#publish` will receive the following parameters:
|
148
|
+
```
|
149
|
+
message.inspect # => { user: { id: 2 },
|
150
|
+
event: 'reset_password_event',
|
151
|
+
reason: nil,
|
152
|
+
message: 'Password reset successfully',
|
153
|
+
duration: '0.0015',
|
154
|
+
succeeded: true }
|
155
|
+
|
156
|
+
properties.inspect # => { message_id: <SecureRandom.uuid>,
|
157
|
+
routing_key: 'my_app.reset_password_event.success',
|
158
|
+
timestamp: <Time.now.to_i> }
|
159
|
+
```
|
160
|
+
|
161
|
+
|
162
|
+
## Configuration
|
163
|
+
You can change the Happenings configuration by passing a block to the `.configure` method.
|
164
|
+
If you're using Happenings with Rails, a good place for this setup is in an
|
165
|
+
initializer such as `config/initializers/happenings.rb`:
|
166
|
+
|
167
|
+
```
|
168
|
+
Happenings.configure do |config|
|
169
|
+
config.logger = your_logger
|
170
|
+
config.publisher = your_publisher
|
171
|
+
config.app_name = 'my awesome app'
|
172
|
+
config.socks = 'black socks'
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
## Requirements
|
177
|
+
|
178
|
+
* ActiveSupport (>= 2.3)
|
179
|
+
|
180
|
+
## Contributing
|
181
|
+
|
182
|
+
1. Fork it ( http://github.com/desmondmonster/happenings/fork )
|
183
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
184
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
185
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
186
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/happenings.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'happenings/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "happenings"
|
8
|
+
spec.version = Happenings::VERSION
|
9
|
+
spec.authors = ["Desmond Bowe"]
|
10
|
+
spec.email = ["desmondbowe@gmail.com"]
|
11
|
+
spec.summary = %q{Event-Driven Domain Scaffold}
|
12
|
+
spec.description = %q{For use in applications where business domain events are first-class citizens}
|
13
|
+
spec.homepage = "https://github.com/desmondmonster/happenings"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
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_dependency 'activesupport', '>= 2.3'
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
23
|
+
spec.add_development_dependency 'pry'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Happenings
|
2
|
+
class Config
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
set_default_attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_default_attributes
|
10
|
+
self.logger = default_logger
|
11
|
+
self.publisher = default_publisher
|
12
|
+
self.app_name = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing method, *args
|
16
|
+
if method =~ /=$/
|
17
|
+
attribute = method.to_s.sub '=', ''
|
18
|
+
self.class.instance_eval { attr_accessor attribute }
|
19
|
+
self.send method, args.first
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def default_logger
|
29
|
+
Logger.new $stdout
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_publisher
|
33
|
+
NullPublisher
|
34
|
+
end
|
35
|
+
|
36
|
+
class NullPublisher
|
37
|
+
def self.publish payload, options
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Happenings
|
2
|
+
|
3
|
+
class OutcomeError < StandardError; end
|
4
|
+
|
5
|
+
module Event
|
6
|
+
|
7
|
+
require 'active_support/inflector'
|
8
|
+
|
9
|
+
attr_reader :duration, :message, :reason, :succeeded
|
10
|
+
|
11
|
+
def run!
|
12
|
+
time do
|
13
|
+
strategy
|
14
|
+
end
|
15
|
+
|
16
|
+
raise OutcomeError.new 'no outcome specified' if no_outcome_specified?
|
17
|
+
|
18
|
+
publish
|
19
|
+
|
20
|
+
succeeded?
|
21
|
+
end
|
22
|
+
|
23
|
+
def strategy
|
24
|
+
success!
|
25
|
+
end
|
26
|
+
|
27
|
+
def success! options = {}
|
28
|
+
result true, options
|
29
|
+
end
|
30
|
+
|
31
|
+
def failure! options = {}
|
32
|
+
result false, options
|
33
|
+
end
|
34
|
+
|
35
|
+
def succeeded?
|
36
|
+
succeeded
|
37
|
+
end
|
38
|
+
|
39
|
+
def payload
|
40
|
+
{}
|
41
|
+
end
|
42
|
+
|
43
|
+
def routing_key
|
44
|
+
[routing_key_prefixes, event_name, outcome].compact.join '.'
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def result succeeded, options
|
51
|
+
@succeeded = succeeded
|
52
|
+
@message = options[:message]
|
53
|
+
@reason = options[:reason]
|
54
|
+
end
|
55
|
+
|
56
|
+
# overload in subclass.
|
57
|
+
def routing_key_prefixes
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
def app_name
|
62
|
+
warn "#app_name is deprecated"
|
63
|
+
Happenings.config.app_name && Happenings.config.app_name.gsub(' ', '').underscore
|
64
|
+
end
|
65
|
+
|
66
|
+
def event_name
|
67
|
+
self.class.to_s.split('::').last.underscore
|
68
|
+
end
|
69
|
+
|
70
|
+
def publish
|
71
|
+
# TODO: change to #reverse_merge because that puts the main payload first
|
72
|
+
Happenings.config.publisher.publish additional_info.merge(payload), properties
|
73
|
+
end
|
74
|
+
|
75
|
+
def properties
|
76
|
+
{ message_id: SecureRandom.uuid,
|
77
|
+
routing_key: routing_key,
|
78
|
+
timestamp: Time.now.to_i }
|
79
|
+
end
|
80
|
+
|
81
|
+
def additional_info
|
82
|
+
{ event: event_name,
|
83
|
+
reason: reason,
|
84
|
+
message: message,
|
85
|
+
succeeded: succeeded,
|
86
|
+
duration: formatted_duration }
|
87
|
+
end
|
88
|
+
|
89
|
+
def outcome
|
90
|
+
succeeded? ? 'success' : 'failure'
|
91
|
+
end
|
92
|
+
|
93
|
+
def no_outcome_specified?
|
94
|
+
succeeded.nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
def formatted_duration
|
98
|
+
"%.6f" % @duration
|
99
|
+
end
|
100
|
+
|
101
|
+
def time
|
102
|
+
initial_time = Time.now.to_f
|
103
|
+
yield
|
104
|
+
@duration = Time.now.to_f - initial_time
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/happenings.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
require 'happenings/version'
|
3
|
+
require_relative 'happenings/config'
|
4
|
+
require_relative 'happenings/event'
|
5
|
+
|
6
|
+
|
7
|
+
module Happenings
|
8
|
+
|
9
|
+
def self.configure
|
10
|
+
yield config if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.config
|
14
|
+
@@config ||= Config.new
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'config' do
|
4
|
+
|
5
|
+
context 'with default settings' do
|
6
|
+
it 'uses the library defaults' do
|
7
|
+
expect(Happenings.config.logger).to be_a Logger
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'when settings are specified' do
|
12
|
+
it 'uses the new settings' do
|
13
|
+
require 'logger'
|
14
|
+
|
15
|
+
class FooLogger < Logger; end
|
16
|
+
|
17
|
+
Happenings.configure do |config|
|
18
|
+
config.logger = FooLogger.new $stdout
|
19
|
+
end
|
20
|
+
|
21
|
+
expect(Happenings.config.logger).to be_a FooLogger
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when random settings are specified' do
|
26
|
+
it 'stores them in the config' do
|
27
|
+
Happenings.configure {|c| c.socks = 'pants'}
|
28
|
+
expect(Happenings.config.socks).to eq 'pants'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'a happening event' do
|
4
|
+
|
5
|
+
let(:event) { Event.new }
|
6
|
+
|
7
|
+
describe '#run!' do
|
8
|
+
context 'when no result action is called' do
|
9
|
+
before { class Event; def strategy; end; end }
|
10
|
+
|
11
|
+
it 'raises an exception' do
|
12
|
+
expect {event.run!}.to raise_error Happenings::OutcomeError
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when the strategy is successful' do
|
17
|
+
before do
|
18
|
+
class Event
|
19
|
+
def strategy
|
20
|
+
success! message: 'it worked'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns true' do
|
26
|
+
expect(event.run!).to be
|
27
|
+
expect(event).to be_succeeded
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'records the duration of the run' do
|
31
|
+
event.run!
|
32
|
+
expect(event.duration).to be > 0.0
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'sets the message' do
|
36
|
+
event.run!
|
37
|
+
expect(event.message).to eq 'it worked'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'publishes the event' do
|
41
|
+
Happenings.config.publisher.should_receive :publish
|
42
|
+
event.run!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when the strategy is unsuccessful' do
|
47
|
+
before do
|
48
|
+
class Event
|
49
|
+
def strategy
|
50
|
+
failure! message: 'it did not work'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns false' do
|
56
|
+
expect(event.run!).not_to be
|
57
|
+
expect(event).not_to be_succeeded
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'records the duration of the run' do
|
61
|
+
event.run!
|
62
|
+
expect(event.duration).to be > 0.0
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'sets the message' do
|
66
|
+
event.run!
|
67
|
+
expect(event.message).to eq 'it did not work'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'publishes the event' do
|
71
|
+
Happenings.config.publisher.should_receive :publish
|
72
|
+
event.run!
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'event publishing' do
|
78
|
+
let(:user) { User.new }
|
79
|
+
let(:password) { 'password' }
|
80
|
+
let(:payload) do
|
81
|
+
{ user: { id: 2 },
|
82
|
+
event: 'reset_password_event',
|
83
|
+
reason: nil,
|
84
|
+
message: message,
|
85
|
+
succeeded: succeeded }
|
86
|
+
end
|
87
|
+
let(:properties) { { routing_key: "reset_password_event.#{outcome}" } }
|
88
|
+
|
89
|
+
context 'when the strategy is successful' do
|
90
|
+
let(:confirmation) { password }
|
91
|
+
let(:succeeded) { true }
|
92
|
+
let(:message) { 'Password reset successfully' }
|
93
|
+
let(:outcome) { 'success' }
|
94
|
+
|
95
|
+
it 'publishes the event' do
|
96
|
+
Happenings.config.publisher.should_receive(:publish)
|
97
|
+
.with(hash_including(:duration, payload), hash_including(:message_id, :timestamp, properties))
|
98
|
+
|
99
|
+
ResetPasswordEvent.new(user, password, confirmation).run!
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'when the strategy is unsuccessful' do
|
104
|
+
let(:confirmation) { 'not the password' }
|
105
|
+
let(:succeeded) { false }
|
106
|
+
let(:message) { 'Password must match confirmation' }
|
107
|
+
let(:outcome) { 'failure' }
|
108
|
+
|
109
|
+
it 'publishes the event' do
|
110
|
+
Happenings.config.publisher.should_receive(:publish)
|
111
|
+
.with(hash_including(:duration, payload), hash_including(:message_id, :timestamp, properties))
|
112
|
+
|
113
|
+
ResetPasswordEvent.new(user, password, confirmation).run!
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
require 'bundler/setup'
|
3
|
+
Bundler.require(:default, :development)
|
4
|
+
|
5
|
+
|
6
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
7
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
8
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
9
|
+
# loaded once.
|
10
|
+
#
|
11
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
12
|
+
|
13
|
+
Dir["#{Dir.pwd}/spec/support/**/*.rb"].each {|f| require f}
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
17
|
+
config.run_all_when_everything_filtered = true
|
18
|
+
config.filter_run :focus
|
19
|
+
|
20
|
+
# Run specs in random order to surface order dependencies. If you find an
|
21
|
+
# order dependency and want to debug it, you can fix the order by providing
|
22
|
+
# the seed, which is printed after each run.
|
23
|
+
# --seed 1234
|
24
|
+
config.order = 'random'
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class ResetPasswordEvent
|
2
|
+
|
3
|
+
include Happenings::Event
|
4
|
+
|
5
|
+
attr_reader :user, :new_password, :new_password_confirmation
|
6
|
+
|
7
|
+
def initialize user, new_password, new_password_confirmation
|
8
|
+
@user = user
|
9
|
+
@new_password = new_password
|
10
|
+
@new_password_confirmation = new_password_confirmation
|
11
|
+
end
|
12
|
+
|
13
|
+
def strategy
|
14
|
+
ensure_passwords_match and
|
15
|
+
reset_user_password
|
16
|
+
end
|
17
|
+
|
18
|
+
def payload
|
19
|
+
{ user: { id: user.id } }
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def reset_user_password
|
26
|
+
user.reset_password! new_password
|
27
|
+
success! message: 'Password reset successfully'
|
28
|
+
end
|
29
|
+
|
30
|
+
def ensure_passwords_match
|
31
|
+
new_password == new_password_confirmation or
|
32
|
+
failure! message: 'Password must match confirmation'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: happenings
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Desmond Bowe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
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: rake
|
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
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: For use in applications where business domain events are first-class
|
84
|
+
citizens
|
85
|
+
email:
|
86
|
+
- desmondbowe@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- .gitignore
|
92
|
+
- .rspec
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- happenings.gemspec
|
98
|
+
- lib/happenings.rb
|
99
|
+
- lib/happenings/config.rb
|
100
|
+
- lib/happenings/event.rb
|
101
|
+
- lib/happenings/version.rb
|
102
|
+
- spec/happenings/configuration_spec.rb
|
103
|
+
- spec/happenings/event_spec.rb
|
104
|
+
- spec/spec_helper.rb
|
105
|
+
- spec/support/event.rb
|
106
|
+
- spec/support/reset_password_event.rb
|
107
|
+
- spec/support/user.rb
|
108
|
+
homepage: https://github.com/desmondmonster/happenings
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 2.0.3
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: Event-Driven Domain Scaffold
|
132
|
+
test_files:
|
133
|
+
- spec/happenings/configuration_spec.rb
|
134
|
+
- spec/happenings/event_spec.rb
|
135
|
+
- spec/spec_helper.rb
|
136
|
+
- spec/support/event.rb
|
137
|
+
- spec/support/reset_password_event.rb
|
138
|
+
- spec/support/user.rb
|