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.
@@ -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
- [@object, past_participle, 'at'].compact.join('_')
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
- [@object, past_participle].compact.join('_')
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
- [@infinitive, @object].compact.join('_')
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
- [@object, past_participle].compact.join('_')
42
+ concatenate(object, past_participle)
25
43
  end
26
44
 
27
45
  def inverse_scope
28
- [@object, 'not', past_participle].compact.join('_')
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
- @infinitive.verb.conjugate(tense: :past, aspect: :perfective)
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
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Events
3
- VERSION = '2.0.0'
3
+ VERSION = '4.0.1'.freeze
4
4
  end
5
5
  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
- context 'without an object' do
5
- subject { described_class.new(:complete) }
4
+ let(:event_name) { :complete }
5
+ let(:options) { {} }
6
6
 
7
- it 'generates a field name' do
8
- expect(subject.field).to eq('completed_at')
9
- end
7
+ subject { described_class.new(event_name, options) }
10
8
 
11
- it 'generates a predicate name' do
12
- expect(subject.predicate).to eq('completed')
13
- end
9
+ it 'generates a field name' do
10
+ expect(subject.field).to eq('completed_at')
11
+ end
14
12
 
15
- it 'generates an action name' do
16
- expect(subject.action).to eq('complete')
17
- end
13
+ it 'generates a predicate name' do
14
+ expect(subject.predicate).to eq('completed?')
15
+ end
18
16
 
19
- it 'generates a scope name' do
20
- expect(subject.scope).to eq('completed')
21
- end
17
+ it 'generates an inverse predicate name' do
18
+ expect(subject.inverse_predicate).to eq('not_completed?')
19
+ end
22
20
 
23
- it 'generates an inverse scope name' do
24
- expect(subject.inverse_scope).to eq('not_completed')
25
- end
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
- subject { described_class.new(:confirm, object: :email) }
42
+ let(:options) { { object: :task } }
30
43
 
31
44
  it 'generates a field name' do
32
- expect(subject.field).to eq('email_confirmed_at')
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('email_confirmed')
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('confirm_email')
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('email_confirmed')
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('email_not_confirmed')
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
- let(:task) { create(:task) }
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 be_completed
10
- expect(task.completed_at).to eq(Time.now)
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.now)
32
+ expect(task.completed_at).to eq(Time.current)
25
33
  end
26
34
 
27
- it 'defines scope methods' do
28
- expect(Task.not_completed).to include(task)
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
- let(:user) { create(:user) }
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
- it 'handles an object' do
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