active_record-events 1.1.0 → 4.0.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.
@@ -5,34 +5,64 @@ module ActiveRecord
5
5
  class Naming
6
6
  def initialize(infinitive, options = {})
7
7
  @infinitive = infinitive
8
- @subject = options[:subject].presence
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
- [@subject, 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
- [@subject, 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, @subject].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
- [@subject, past_participle].compact.join('_')
42
+ concatenate(object, past_participle)
25
43
  end
26
44
 
27
45
  def inverse_scope
28
- [@subject, '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
- options = { tense: :past, aspect: :perfective }
35
- @infinitive.verb.conjugate(options)
61
+ infinitive.verb.conjugate(tense: :past, aspect: :perfective)
62
+ end
63
+
64
+ def plural_object
65
+ object.to_s.pluralize if object.present?
36
66
  end
37
67
  end
38
68
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Events
3
- VERSION = '1.1.0'
3
+ VERSION = '4.0.0'.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 a subject' do
5
- subject { described_class.new(:complete) }
4
+ let(:event_name) { :complete }
5
+ let(:options) { {} }
6
+
7
+ subject { described_class.new(event_name, options) }
8
+
9
+ it 'generates a field name' do
10
+ expect(subject.field).to eq('completed_at')
11
+ end
12
+
13
+ it 'generates a predicate name' do
14
+ expect(subject.predicate).to eq('completed?')
15
+ end
16
+
17
+ it 'generates an inverse predicate name' do
18
+ expect(subject.inverse_predicate).to eq('not_completed?')
19
+ end
20
+
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')
39
+ end
40
+
41
+ context 'with an object' do
42
+ let(:options) { { object: :task } }
6
43
 
7
44
  it 'generates a field name' do
8
- expect(subject.field).to eq('completed_at')
45
+ expect(subject.field).to eq('task_completed_at')
9
46
  end
10
47
 
11
48
  it 'generates a predicate name' do
12
- expect(subject.predicate).to eq('completed')
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?')
13
54
  end
14
55
 
15
56
  it 'generates an action name' do
16
- expect(subject.action).to eq('complete')
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')
17
66
  end
18
67
 
19
68
  it 'generates a scope name' do
20
- expect(subject.scope).to eq('completed')
69
+ expect(subject.scope).to eq('task_completed')
21
70
  end
22
71
 
23
72
  it 'generates an inverse scope name' do
24
- expect(subject.inverse_scope).to eq('not_completed')
73
+ expect(subject.inverse_scope).to eq('task_not_completed')
25
74
  end
26
75
  end
27
76
 
28
- context 'with a subject' do
29
- subject { described_class.new(:confirm, subject: :email) }
77
+ context 'with a date field' do
78
+ let(:options) { { field_type: :date } }
30
79
 
31
80
  it 'generates a field name' do
32
- expect(subject.field).to eq('email_confirmed_at')
33
- end
34
-
35
- it 'generates a predicate name' do
36
- expect(subject.predicate).to eq('email_confirmed')
37
- end
38
-
39
- it 'generates an action name' do
40
- expect(subject.action).to eq('confirm_email')
81
+ expect(subject.field).to eq('completed_on')
41
82
  end
83
+ end
42
84
 
43
- it 'generates a scope name' do
44
- expect(subject.scope).to eq('email_confirmed')
45
- end
85
+ context 'with a custom field name' do
86
+ let(:options) { { field_name: :completion_time } }
46
87
 
47
- it 'generates an inverse scope name' do
48
- expect(subject.inverse_scope).to eq('email_not_confirmed')
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,39 @@ 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
+ it 'records multiple timestamps at once' do
36
+ Task.complete_all
37
+
38
+ expect(task.reload).to be_completed
39
+ end
40
+
41
+ it 'defines a scope' do
29
42
  expect(Task.completed).not_to include(task)
30
43
  end
31
44
 
32
- let(:user) { create(:user) }
45
+ it 'defines an inverse scope' do
46
+ expect(Task.not_completed).to include(task)
47
+ end
48
+
49
+ it 'allows overriding instance methods' do
50
+ expect(ActiveRecord::Base.logger).to receive(:info)
51
+
52
+ task.complete!
53
+
54
+ expect(task).to be_completed
55
+ end
56
+
57
+ it 'allows overriding class methods' do
58
+ expect(ActiveRecord::Base.logger).to receive(:info)
59
+
60
+ Task.complete_all
33
61
 
34
- it 'handles a subject' do
35
- user.confirm_email
36
- expect(user.email_confirmed?).to be(true)
62
+ expect(task.reload).to be_completed
37
63
  end
38
64
  end