active_record-events 2.0.0 → 4.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 +5 -13
- data/{MIT-LICENSE → LICENSE} +0 -0
- data/README.md +139 -12
- data/Rakefile +27 -3
- data/lib/active_record/events.rb +6 -32
- data/lib/active_record/events/extension.rb +19 -0
- data/lib/active_record/events/macro.rb +36 -0
- data/lib/active_record/events/method_factory.rb +88 -0
- data/lib/active_record/events/naming.rb +37 -6
- data/lib/active_record/events/version.rb +1 -1
- data/lib/generators/active_record/event/USAGE +15 -0
- data/lib/generators/active_record/event/event_generator.rb +60 -0
- data/spec/active_record/events/macro_spec.rb +49 -0
- data/spec/active_record/events/method_factory_spec.rb +59 -0
- data/spec/active_record/events/naming_spec.rb +64 -23
- data/spec/active_record/events_spec.rb +46 -10
- data/spec/dummy/app/models/task.rb +11 -0
- data/spec/dummy/config/boot.rb +4 -3
- data/spec/dummy/config/database.yml +3 -3
- data/spec/dummy/config/environment.rb +6 -4
- data/spec/dummy/db/migrate/20150813132804_create_tasks.rb +1 -1
- data/spec/dummy/db/schema.rb +5 -12
- data/spec/generators/active_record/event/event_generator_spec.rb +45 -0
- data/spec/spec_helper.rb +10 -10
- data/spec/support/{factories.rb → factories/task_factory.rb} +0 -1
- data/spec/support/helpers/generator_helpers.rb +8 -0
- data/spec/support/matchers/have_method.rb +5 -0
- metadata +107 -59
- data/spec/dummy/app/models/user.rb +0 -3
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20170103215307_create_users.rb +0 -9
- data/spec/dummy/db/test.sqlite3 +0 -0
@@ -6,32 +6,63 @@ module ActiveRecord
|
|
6
6
|
def initialize(infinitive, options = {})
|
7
7
|
@infinitive = infinitive
|
8
8
|
@object = options[:object].presence
|
9
|
+
@field_name = options[:field_name].to_s
|
10
|
+
@field_type = options[:field_type].try(:to_sym)
|
9
11
|
end
|
10
12
|
|
11
13
|
def field
|
12
|
-
|
14
|
+
return field_name if field_name.present?
|
15
|
+
|
16
|
+
suffix = field_type == :date ? 'on' : 'at'
|
17
|
+
|
18
|
+
concatenate(object, past_participle, suffix)
|
13
19
|
end
|
14
20
|
|
15
21
|
def predicate
|
16
|
-
|
22
|
+
concatenate(object, past_participle) + '?'
|
23
|
+
end
|
24
|
+
|
25
|
+
def inverse_predicate
|
26
|
+
concatenate(object, 'not', past_participle) + '?'
|
17
27
|
end
|
18
28
|
|
19
29
|
def action
|
20
|
-
|
30
|
+
concatenate(infinitive, object) + '!'
|
31
|
+
end
|
32
|
+
|
33
|
+
def safe_action
|
34
|
+
concatenate(infinitive, object)
|
35
|
+
end
|
36
|
+
|
37
|
+
def collective_action
|
38
|
+
concatenate(infinitive, 'all', plural_object)
|
21
39
|
end
|
22
40
|
|
23
41
|
def scope
|
24
|
-
|
42
|
+
concatenate(object, past_participle)
|
25
43
|
end
|
26
44
|
|
27
45
|
def inverse_scope
|
28
|
-
|
46
|
+
concatenate(object, 'not', past_participle)
|
29
47
|
end
|
30
48
|
|
31
49
|
private
|
32
50
|
|
51
|
+
attr_reader :infinitive
|
52
|
+
attr_reader :object
|
53
|
+
attr_reader :field_name
|
54
|
+
attr_reader :field_type
|
55
|
+
|
56
|
+
def concatenate(*parts)
|
57
|
+
parts.compact.join('_')
|
58
|
+
end
|
59
|
+
|
33
60
|
def past_participle
|
34
|
-
|
61
|
+
infinitive.verb.conjugate(tense: :past, aspect: :perfective)
|
62
|
+
end
|
63
|
+
|
64
|
+
def plural_object
|
65
|
+
object.to_s.pluralize if object.present?
|
35
66
|
end
|
36
67
|
end
|
37
68
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Description:
|
2
|
+
Generates a database migration and updates the model file (if it exists).
|
3
|
+
Pass the model name, either CamelCased or under_scored, and the event name
|
4
|
+
as an infinitive.
|
5
|
+
|
6
|
+
Examples:
|
7
|
+
`bin/rails generate active_record:event task complete`
|
8
|
+
|
9
|
+
Generates a migration adding a `completed_at` column to the `tasks`
|
10
|
+
table and updates the Task model.
|
11
|
+
|
12
|
+
`bin/rails generate active_record:event user confirm --object=email`
|
13
|
+
|
14
|
+
Generates a migration adding an `email_confirmed_at` column to the
|
15
|
+
`users` table and updates the User model.
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
require 'active_record/events/naming'
|
4
|
+
require 'active_record/events/macro'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Generators
|
8
|
+
class EventGenerator < Rails::Generators::Base
|
9
|
+
MACRO_OPTIONS = %w[object field_type skip_scopes].freeze
|
10
|
+
|
11
|
+
argument :model_name, type: :string
|
12
|
+
argument :event_name, type: :string
|
13
|
+
|
14
|
+
class_option :skip_scopes, type: :boolean,
|
15
|
+
desc: 'Skip the inclusion of scope methods'
|
16
|
+
class_option :field_type, type: :string,
|
17
|
+
desc: 'The field type (datetime or date)'
|
18
|
+
class_option :object, type: :string,
|
19
|
+
desc: 'The name of the object'
|
20
|
+
|
21
|
+
source_root File.expand_path('templates', __dir__)
|
22
|
+
|
23
|
+
def generate_migration_file
|
24
|
+
naming = ActiveRecord::Events::Naming.new(event_name, options)
|
25
|
+
|
26
|
+
table_name = model_name.tableize
|
27
|
+
field_name = naming.field
|
28
|
+
field_type = options[:field_type] || 'datetime'
|
29
|
+
|
30
|
+
migration_name = "add_#{field_name}_to_#{table_name}"
|
31
|
+
attributes = "#{field_name}:#{field_type}"
|
32
|
+
|
33
|
+
invoke 'active_record:migration', [migration_name, attributes]
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_model_file
|
37
|
+
return unless model_file_exists?
|
38
|
+
|
39
|
+
macro_options = options.slice(*MACRO_OPTIONS)
|
40
|
+
macro = ActiveRecord::Events::Macro.new(event_name, macro_options)
|
41
|
+
|
42
|
+
inject_into_file model_file_path, "\s\s#{macro}\n", after: /class.+\n/
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def model_file_exists?
|
48
|
+
File.exist?(model_file_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def model_file_path
|
52
|
+
File.expand_path("app/models/#{model_file_name}", destination_root)
|
53
|
+
end
|
54
|
+
|
55
|
+
def model_file_name
|
56
|
+
"#{model_name.underscore.singularize}.rb"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record/events/macro'
|
3
|
+
|
4
|
+
RSpec.describe ActiveRecord::Events::Macro do
|
5
|
+
let(:event_name) { :confirm }
|
6
|
+
|
7
|
+
subject { described_class.new(event_name, options) }
|
8
|
+
|
9
|
+
context 'without options' do
|
10
|
+
let(:options) { {} }
|
11
|
+
|
12
|
+
it "doesn't include any options" do
|
13
|
+
expect(subject.to_s).to eq('has_event :confirm')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with a string option' do
|
18
|
+
let(:options) { { object: 'email' } }
|
19
|
+
|
20
|
+
it 'prepends the option value with a colon' do
|
21
|
+
expect(subject.to_s).to eq('has_event :confirm, object: :email')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with a symbol option' do
|
26
|
+
let(:options) { { object: :email } }
|
27
|
+
|
28
|
+
it 'prepends the option value with a colon' do
|
29
|
+
expect(subject.to_s).to eq('has_event :confirm, object: :email')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with a boolean option' do
|
34
|
+
let(:options) { { skip_scopes: true } }
|
35
|
+
|
36
|
+
it "doesn't prepend the option value with a colon" do
|
37
|
+
expect(subject.to_s).to eq('has_event :confirm, skip_scopes: true')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with multiple options' do
|
42
|
+
let(:options) { { object: :email, field_type: :date } }
|
43
|
+
|
44
|
+
it 'includes all of the options' do
|
45
|
+
macro = 'has_event :confirm, object: :email, field_type: :date'
|
46
|
+
expect(subject.to_s).to eq(macro)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveRecord::Events::MethodFactory do
|
4
|
+
let(:event_name) { :complete }
|
5
|
+
let(:options) { {} }
|
6
|
+
|
7
|
+
subject { described_class.new(event_name, options) }
|
8
|
+
|
9
|
+
it 'generates instance methods' do
|
10
|
+
result = subject.instance_methods
|
11
|
+
|
12
|
+
expect(result).to have_method(:completed?)
|
13
|
+
expect(result).to have_method(:not_completed?)
|
14
|
+
expect(result).to have_method(:complete!)
|
15
|
+
expect(result).to have_method(:complete)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'generates class methods' do
|
19
|
+
result = subject.class_methods
|
20
|
+
|
21
|
+
expect(result).to have_method(:complete_all)
|
22
|
+
expect(result).to have_method(:completed)
|
23
|
+
expect(result).to have_method(:not_completed)
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'with the object option' do
|
27
|
+
let(:options) { { object: :task } }
|
28
|
+
|
29
|
+
it 'generates instance methods' do
|
30
|
+
result = subject.instance_methods
|
31
|
+
|
32
|
+
expect(result).to have_method(:task_completed?)
|
33
|
+
expect(result).to have_method(:task_not_completed?)
|
34
|
+
expect(result).to have_method(:complete_task!)
|
35
|
+
expect(result).to have_method(:complete_task)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'generates class methods' do
|
39
|
+
result = subject.class_methods
|
40
|
+
|
41
|
+
expect(result).to have_method(:complete_all_tasks)
|
42
|
+
expect(result).to have_method(:task_completed)
|
43
|
+
expect(result).to have_method(:task_not_completed)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'with the skip scopes option' do
|
48
|
+
let(:options) { { skip_scopes: true } }
|
49
|
+
|
50
|
+
it 'generates class methods without scopes' do
|
51
|
+
result = subject.class_methods
|
52
|
+
|
53
|
+
expect(result).to have_method(:complete_all)
|
54
|
+
|
55
|
+
expect(result).not_to have_method(:completed)
|
56
|
+
expect(result).not_to have_method(:not_completed)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,51 +1,92 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ActiveRecord::Events::Naming do
|
4
|
-
|
5
|
-
|
4
|
+
let(:event_name) { :complete }
|
5
|
+
let(:options) { {} }
|
6
6
|
|
7
|
-
|
8
|
-
expect(subject.field).to eq('completed_at')
|
9
|
-
end
|
7
|
+
subject { described_class.new(event_name, options) }
|
10
8
|
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
it 'generates a field name' do
|
10
|
+
expect(subject.field).to eq('completed_at')
|
11
|
+
end
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
it 'generates a predicate name' do
|
14
|
+
expect(subject.predicate).to eq('completed?')
|
15
|
+
end
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
it 'generates an inverse predicate name' do
|
18
|
+
expect(subject.inverse_predicate).to eq('not_completed?')
|
19
|
+
end
|
22
20
|
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
it 'generates an action name' do
|
22
|
+
expect(subject.action).to eq('complete!')
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'generates a safe action name' do
|
26
|
+
expect(subject.safe_action).to eq('complete')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'generates a collective action name' do
|
30
|
+
expect(subject.collective_action).to eq('complete_all')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'generates a scope name' do
|
34
|
+
expect(subject.scope).to eq('completed')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'generates an inverse scope name' do
|
38
|
+
expect(subject.inverse_scope).to eq('not_completed')
|
26
39
|
end
|
27
40
|
|
28
41
|
context 'with an object' do
|
29
|
-
|
42
|
+
let(:options) { { object: :task } }
|
30
43
|
|
31
44
|
it 'generates a field name' do
|
32
|
-
expect(subject.field).to eq('
|
45
|
+
expect(subject.field).to eq('task_completed_at')
|
33
46
|
end
|
34
47
|
|
35
48
|
it 'generates a predicate name' do
|
36
|
-
expect(subject.predicate).to eq('
|
49
|
+
expect(subject.predicate).to eq('task_completed?')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'generates an inverse predicate name' do
|
53
|
+
expect(subject.inverse_predicate).to eq('task_not_completed?')
|
37
54
|
end
|
38
55
|
|
39
56
|
it 'generates an action name' do
|
40
|
-
expect(subject.action).to eq('
|
57
|
+
expect(subject.action).to eq('complete_task!')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'generates a safe action name' do
|
61
|
+
expect(subject.safe_action).to eq('complete_task')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'generates a collective action name' do
|
65
|
+
expect(subject.collective_action).to eq('complete_all_tasks')
|
41
66
|
end
|
42
67
|
|
43
68
|
it 'generates a scope name' do
|
44
|
-
expect(subject.scope).to eq('
|
69
|
+
expect(subject.scope).to eq('task_completed')
|
45
70
|
end
|
46
71
|
|
47
72
|
it 'generates an inverse scope name' do
|
48
|
-
expect(subject.inverse_scope).to eq('
|
73
|
+
expect(subject.inverse_scope).to eq('task_not_completed')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with a date field' do
|
78
|
+
let(:options) { { field_type: :date } }
|
79
|
+
|
80
|
+
it 'generates a field name' do
|
81
|
+
expect(subject.field).to eq('completed_on')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with a custom field name' do
|
86
|
+
let(:options) { { field_name: :completion_time } }
|
87
|
+
|
88
|
+
it 'returns the field name' do
|
89
|
+
expect(subject.field).to eq('completion_time')
|
49
90
|
end
|
50
91
|
end
|
51
92
|
end
|
@@ -1,17 +1,24 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ActiveRecord::Events do
|
4
|
-
|
4
|
+
around(:each) do |example|
|
5
|
+
Timecop.freeze { example.run }
|
6
|
+
end
|
7
|
+
|
8
|
+
let!(:task) { create(:task) }
|
5
9
|
|
6
10
|
it 'records a timestamp' do
|
7
11
|
task.complete
|
8
12
|
|
9
|
-
expect(task).to
|
10
|
-
expect(task.
|
13
|
+
expect(task.completed?).to eq(true)
|
14
|
+
expect(task.not_completed?).to eq(false)
|
15
|
+
|
16
|
+
expect(task.completed_at).to eq(Time.current)
|
11
17
|
end
|
12
18
|
|
13
19
|
it 'preserves a timestamp' do
|
14
20
|
task = create(:task, completed_at: 3.days.ago)
|
21
|
+
|
15
22
|
task.complete
|
16
23
|
|
17
24
|
expect(task.completed_at).to eq(3.days.ago)
|
@@ -19,20 +26,49 @@ RSpec.describe ActiveRecord::Events do
|
|
19
26
|
|
20
27
|
it 'updates a timestamp' do
|
21
28
|
task = create(:task, completed_at: 3.days.ago)
|
29
|
+
|
22
30
|
task.complete!
|
23
31
|
|
24
|
-
expect(task.completed_at).to eq(Time.
|
32
|
+
expect(task.completed_at).to eq(Time.current)
|
25
33
|
end
|
26
34
|
|
27
|
-
|
28
|
-
|
35
|
+
context 'with a non-persisted object' do
|
36
|
+
it 'updates the timestamp' do
|
37
|
+
task = build(:task, completed_at: 3.days.ago)
|
38
|
+
|
39
|
+
task.complete!
|
40
|
+
|
41
|
+
expect(task.completed_at).to eq(Time.current)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'records multiple timestamps at once' do
|
46
|
+
Task.complete_all
|
47
|
+
|
48
|
+
expect(task.reload).to be_completed
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'defines a scope' do
|
29
52
|
expect(Task.completed).not_to include(task)
|
30
53
|
end
|
31
54
|
|
32
|
-
|
55
|
+
it 'defines an inverse scope' do
|
56
|
+
expect(Task.not_completed).to include(task)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'allows overriding instance methods' do
|
60
|
+
expect(ActiveRecord::Base.logger).to receive(:info)
|
61
|
+
|
62
|
+
task.complete!
|
63
|
+
|
64
|
+
expect(task).to be_completed
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'allows overriding class methods' do
|
68
|
+
expect(ActiveRecord::Base.logger).to receive(:info)
|
69
|
+
|
70
|
+
Task.complete_all
|
33
71
|
|
34
|
-
|
35
|
-
user.confirm_email
|
36
|
-
expect(user.email_confirmed?).to be(true)
|
72
|
+
expect(task.reload).to be_completed
|
37
73
|
end
|
38
74
|
end
|