active_record-events 2.0.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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