deferring 0.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.
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.1.0"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ raincheck (0.0.1)
5
+ activerecord (> 3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.1.0)
11
+ activesupport (= 4.1.0)
12
+ builder (~> 3.1)
13
+ activerecord (4.1.0)
14
+ activemodel (= 4.1.0)
15
+ activesupport (= 4.1.0)
16
+ arel (~> 5.0.0)
17
+ activesupport (4.1.0)
18
+ i18n (~> 0.6, >= 0.6.9)
19
+ json (~> 1.7, >= 1.7.7)
20
+ minitest (~> 5.1)
21
+ thread_safe (~> 0.1)
22
+ tzinfo (~> 1.1)
23
+ appraisal (1.0.0)
24
+ bundler
25
+ rake
26
+ thor (>= 0.14.0)
27
+ arel (5.0.1.20140414130214)
28
+ builder (3.2.2)
29
+ diff-lcs (1.2.5)
30
+ i18n (0.6.9)
31
+ json (1.8.1)
32
+ minitest (5.3.3)
33
+ rake (10.3.1)
34
+ rspec (2.14.1)
35
+ rspec-core (~> 2.14.0)
36
+ rspec-expectations (~> 2.14.0)
37
+ rspec-mocks (~> 2.14.0)
38
+ rspec-core (2.14.8)
39
+ rspec-expectations (2.14.5)
40
+ diff-lcs (>= 1.1.3, < 2.0)
41
+ rspec-mocks (2.14.6)
42
+ sqlite3 (1.3.9)
43
+ thor (0.19.1)
44
+ thread_safe (0.3.3)
45
+ tzinfo (1.1.0)
46
+ thread_safe (~> 0.1)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ activerecord (= 4.1.0)
53
+ appraisal
54
+ bundler (~> 1.3)
55
+ raincheck!
56
+ rake
57
+ rspec
58
+ sqlite3
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.0.4"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ deferring (0.0.1)
5
+ activerecord (> 3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.0.4)
11
+ activesupport (= 4.0.4)
12
+ builder (~> 3.1.0)
13
+ activerecord (4.0.4)
14
+ activemodel (= 4.0.4)
15
+ activerecord-deprecated_finders (~> 1.0.2)
16
+ activesupport (= 4.0.4)
17
+ arel (~> 4.0.0)
18
+ activerecord-deprecated_finders (1.0.3)
19
+ activesupport (4.0.4)
20
+ i18n (~> 0.6, >= 0.6.9)
21
+ minitest (~> 4.2)
22
+ multi_json (~> 1.3)
23
+ thread_safe (~> 0.1)
24
+ tzinfo (~> 0.3.37)
25
+ appraisal (1.0.0)
26
+ bundler
27
+ rake
28
+ thor (>= 0.14.0)
29
+ arel (4.0.2)
30
+ builder (3.1.4)
31
+ diff-lcs (1.2.5)
32
+ i18n (0.6.9)
33
+ minitest (4.7.5)
34
+ multi_json (1.9.2)
35
+ rake (10.3.1)
36
+ rspec (2.14.1)
37
+ rspec-core (~> 2.14.0)
38
+ rspec-expectations (~> 2.14.0)
39
+ rspec-mocks (~> 2.14.0)
40
+ rspec-core (2.14.8)
41
+ rspec-expectations (2.14.5)
42
+ diff-lcs (>= 1.1.3, < 2.0)
43
+ rspec-mocks (2.14.6)
44
+ sqlite3 (1.3.9)
45
+ thor (0.19.1)
46
+ thread_safe (0.3.3)
47
+ tzinfo (0.3.39)
48
+
49
+ PLATFORMS
50
+ ruby
51
+
52
+ DEPENDENCIES
53
+ activerecord (= 4.0.4)
54
+ appraisal
55
+ bundler (~> 1.3)
56
+ deferring!
57
+ rake
58
+ rspec
59
+ sqlite3
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.1.0"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ deferring (0.0.1)
5
+ activerecord (> 3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.1.0)
11
+ activesupport (= 4.1.0)
12
+ builder (~> 3.1)
13
+ activerecord (4.1.0)
14
+ activemodel (= 4.1.0)
15
+ activesupport (= 4.1.0)
16
+ arel (~> 5.0.0)
17
+ activesupport (4.1.0)
18
+ i18n (~> 0.6, >= 0.6.9)
19
+ json (~> 1.7, >= 1.7.7)
20
+ minitest (~> 5.1)
21
+ thread_safe (~> 0.1)
22
+ tzinfo (~> 1.1)
23
+ appraisal (1.0.0)
24
+ bundler
25
+ rake
26
+ thor (>= 0.14.0)
27
+ arel (5.0.1.20140414130214)
28
+ builder (3.2.2)
29
+ diff-lcs (1.2.5)
30
+ i18n (0.6.9)
31
+ json (1.8.1)
32
+ minitest (5.3.3)
33
+ rake (10.3.1)
34
+ rspec (2.14.1)
35
+ rspec-core (~> 2.14.0)
36
+ rspec-expectations (~> 2.14.0)
37
+ rspec-mocks (~> 2.14.0)
38
+ rspec-core (2.14.8)
39
+ rspec-expectations (2.14.5)
40
+ diff-lcs (>= 1.1.3, < 2.0)
41
+ rspec-mocks (2.14.6)
42
+ sqlite3 (1.3.9)
43
+ thor (0.19.1)
44
+ thread_safe (0.3.3)
45
+ tzinfo (1.1.0)
46
+ thread_safe (~> 0.1)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ activerecord (= 4.1.0)
53
+ appraisal
54
+ bundler (~> 1.3)
55
+ deferring!
56
+ rake
57
+ rspec
58
+ sqlite3
data/lib/deferring.rb ADDED
@@ -0,0 +1,150 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'deferring/version'
4
+ require 'deferring/deferred_association'
5
+
6
+ module Deferring
7
+ # Creates a wrapper around `has_and_belongs_to_many`. A normal habtm
8
+ # association is created, but this association is wrapped in a
9
+ # DeferredAssociation. The accessor methods of the original association are
10
+ # replaced with ones that will defer saving the association until the parent
11
+ # object has been saved.
12
+ def deferred_has_and_belongs_to_many(*args)
13
+ has_and_belongs_to_many(*args)
14
+ generate_deferred_association_methods(args.first.to_s)
15
+ end
16
+
17
+ # Creates a wrapper around `has_many`. A normal has many association is
18
+ # created, but this association is wrapped in a DeferredAssociation. The
19
+ # accessor methods of the original association are replaced with ones that
20
+ # will defer saving the association until the parent object has been saved.
21
+ def deferred_has_many(*args)
22
+ has_many(*args)
23
+ generate_deferred_association_methods(args.first.to_s)
24
+ end
25
+
26
+ def deferred_accepts_nested_attributes_for(*args)
27
+ accepts_nested_attributes_for(*args)
28
+ association_name = args.first.to_s
29
+
30
+ # teams_attributes=
31
+ define_method :"#{association_name}_attributes=" do |records|
32
+ find_or_create_deferred_association(association_name)
33
+
34
+ # Remove the records that are to be destroyed from the ids that are to be
35
+ # assigned to the DeferredAssociation instance.
36
+ records.reject! { |record| record[:_destroy] }
37
+
38
+ klass = self.class.reflect_on_association(:"#{association_name}").klass
39
+ objects = klass.find(records.map { |record| record[:id] })
40
+ send(:"deferred_#{association_name}").objects = objects
41
+ end
42
+
43
+ generate_find_or_create_deferred_association_method
44
+ end
45
+
46
+ def generate_deferred_association_methods(association_name)
47
+ # Store the original accessor methods of the association.
48
+ alias_method :"original_#{association_name}", :"#{association_name}"
49
+ alias_method :"original_#{association_name}=", :"#{association_name}="
50
+
51
+ # Accessor for our own association.
52
+ attr_accessor :"deferred_#{association_name}"
53
+
54
+ # before/afer remove callbacks
55
+ define_callbacks :"deferred_#{association_name}_save", scope: [:kind, :name]
56
+ define_callbacks :"deferred_#{association_name.singularize}_remove", scope: [:kind, :name]
57
+ define_callbacks :"deferred_#{association_name.singularize}_add", scope: [:kind, :name]
58
+
59
+ # collection
60
+ #
61
+ # Returns an array of all the associated objects. An empty array is returned
62
+ # if none are found.
63
+ # TODO: add force_reload argument?
64
+ define_method :"#{association_name}" do
65
+ find_or_create_deferred_association(association_name)
66
+ send(:"deferred_#{association_name}")
67
+ end
68
+
69
+ # collection=objects
70
+ #
71
+ # Replaces the collection's content by deleting and adding objects as
72
+ # appropriate.
73
+ define_method :"#{association_name}=" do |objects|
74
+ find_or_create_deferred_association(association_name)
75
+ send(:"deferred_#{association_name}").objects = objects
76
+ end
77
+
78
+ # collection_singular_ids=
79
+ #
80
+ # Replace the collection by the objects identified by the primary keys in
81
+ # ids.
82
+ define_method :"#{association_name.singularize}_ids=" do |ids|
83
+ find_or_create_deferred_association(association_name)
84
+
85
+ klass = self.class.reflect_on_association(:"#{association_name}").klass
86
+ objects = klass.find(ids.reject(&:blank?))
87
+ send(:"deferred_#{association_name}").objects = objects
88
+ end
89
+
90
+ # collection_singular_ids
91
+ #
92
+ # Returns an array of the associated objects' ids.
93
+ define_method :"#{association_name.singularize}_ids" do
94
+ find_or_create_deferred_association(association_name)
95
+ send(:"deferred_#{association_name}").ids
96
+ end
97
+
98
+ # collection_singalur_checked
99
+ attr_accessor :"#{association_name}_checked"
100
+ # collection_singalur_checked=
101
+ define_method(:"#{association_name}_checked=") do |ids|
102
+ send(:"#{association_name.singularize}_ids=", ids.split(','))
103
+ end
104
+
105
+ # the save after the parent object has been saved
106
+ after_save :"perform_deferred_#{association_name}_save!"
107
+ define_method :"perform_deferred_#{association_name}_save!" do
108
+ run_callbacks :"deferred_#{association_name}_save" do
109
+ find_or_create_deferred_association(association_name)
110
+
111
+ # Send the objects of our delegated association to the original
112
+ # association and store the result.
113
+ send(:"original_#{association_name}=", send(:"deferred_#{association_name}").objects)
114
+ send(:"deferred_#{association_name}").objects.each do |record|
115
+ record.run_callbacks :commit
116
+ end
117
+
118
+ # Store the new value of the association into our delegated association.
119
+ send(
120
+ :"deferred_#{association_name}=",
121
+ DeferredAssociation.new(send(:"original_#{association_name}"), self, association_name))
122
+ end
123
+ end
124
+
125
+ define_method :"reload_with_deferred_#{association_name}" do |*args|
126
+ find_or_create_deferred_association(association_name)
127
+
128
+ send(:"reload_without_deferred_#{association_name}", *args).tap do
129
+ send(
130
+ :"deferred_#{association_name}=",
131
+ DeferredAssociation.new(send(:"original_#{association_name}"), self, association_name))
132
+ end
133
+ end
134
+ alias_method_chain :reload, :"deferred_#{association_name}"
135
+
136
+ generate_find_or_create_deferred_association_method
137
+ end
138
+
139
+ def generate_find_or_create_deferred_association_method
140
+ define_method :find_or_create_deferred_association do |name|
141
+ if send(:"deferred_#{name}").nil?
142
+ send(
143
+ :"deferred_#{name}=",
144
+ DeferredAssociation.new(send(:"original_#{name}"), self, name))
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ ActiveRecord::Base.send(:extend, Deferring)
@@ -0,0 +1,187 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'delegate'
4
+
5
+ module Deferring
6
+ class DeferredAssociation < SimpleDelegator
7
+ # TODO: Write tests for enumerable.
8
+ include Enumerable
9
+
10
+ attr_reader :load_state
11
+
12
+ def initialize(original_association, obj, name)
13
+ super(original_association)
14
+ @name = name
15
+ @obj = obj
16
+ @load_state = :ghost
17
+ end
18
+
19
+ alias_method :original_association, :__getobj__
20
+
21
+ delegate :to_s, :to_a, :inspect, :==, # methods undefined by SimpleDelegator
22
+ :is_a?, :as_json,
23
+
24
+ :[], :clear, :reject, :reject!, :flatten, :flatten!, :sort!,
25
+ :empty?, :size, :length, # methods on Array
26
+
27
+ to: :objects
28
+
29
+ def each(&block)
30
+ objects.each(&block)
31
+ end
32
+
33
+ # TODO: Add explanation about :first/:last loaded? problem.
34
+ [:first, :last].each do |method|
35
+ define_method method do
36
+ unless objects_loaded?
37
+ original_association.send(method)
38
+ else
39
+ objects.send(method)
40
+ end
41
+ end
42
+ end
43
+
44
+ def find(*args)
45
+ original_association.find(*args)
46
+ end
47
+
48
+ def select(value = Proc.new)
49
+ if block_given?
50
+ objects.select { |*block_args| value.call(*block_args) }
51
+ else
52
+ original_association.select(value)
53
+ end
54
+ end
55
+
56
+ # Rails 3.0 specific, not needed anymore for Rails 3.0+
57
+ def set_inverse_instance(associated_record, parent_record)
58
+ original_association.__send__(:set_inverse_instance, associated_record, parent_record)
59
+ end
60
+
61
+ def association
62
+ load_objects
63
+ original_association
64
+ end
65
+
66
+ def objects
67
+ load_objects
68
+ @objects
69
+ end
70
+
71
+ def original_objects
72
+ load_objects
73
+ @original_objects
74
+ end
75
+
76
+ def objects=(records)
77
+ @objects = records
78
+ @original_objects = original_association.to_a.clone
79
+ objects_loaded!
80
+
81
+ pending_deletes.each do |record|
82
+ # TODO: I don't like the fact that we know something about @obj in here.
83
+ # Refactor to remove that (some kind of notification), it looks
84
+ # terrible this way ;(
85
+ @obj.instance_variable_set(:"@deferred_#{@name.singularize}_remove", record)
86
+ @obj.run_callbacks :"deferred_#{@name.singularize}_remove"
87
+ @obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_remove")
88
+ end
89
+
90
+ pending_creates.each do |record|
91
+ @obj.instance_variable_set(:"@deferred_#{@name.singularize}_add", record)
92
+ @obj.run_callbacks :"deferred_#{@name.singularize}_add"
93
+ @obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_add")
94
+ end
95
+
96
+ @objects
97
+ end
98
+
99
+ def ids
100
+ objects.map(&:id)
101
+ end
102
+
103
+ def <<(records)
104
+ # TODO: Do we want to prevent including the same object twice? Not sure,
105
+ # but it will probably be filtered after saving and retrieving as well.
106
+ Array(records).flatten.uniq.each do |record|
107
+ @obj.instance_variable_set(:"@deferred_#{@name.singularize}_add", record)
108
+ @obj.run_callbacks :"deferred_#{@name.singularize}_add" do
109
+ objects << record
110
+ end
111
+ @obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_add")
112
+ end
113
+ objects
114
+ end
115
+ alias_method :push, :<<
116
+ alias_method :concat, :<<
117
+ alias_method :append, :<<
118
+
119
+ def delete(records)
120
+ Array(records).flatten.uniq.each do |record|
121
+ @obj.instance_variable_set(:"@deferred_#{@name.singularize}_remove", record)
122
+ @obj.run_callbacks :"deferred_#{@name.singularize}_remove" do
123
+ objects.delete(record)
124
+ end
125
+ @obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_remove")
126
+ end
127
+ self
128
+ end
129
+
130
+ def build(*args, &block)
131
+ association.build(*args, &block).tap do |result|
132
+ objects.push(result)
133
+ association.reload
134
+ end
135
+ end
136
+
137
+ def create!(*args, &block)
138
+ association.create!(*args, &block).tap do |result|
139
+ @load_state = :ghost
140
+ load_objects
141
+ end
142
+ end
143
+
144
+ def reload
145
+ original_association.reload
146
+ @load_state = :ghost
147
+ self
148
+ end
149
+ alias_method :reset, :reload
150
+
151
+ # Returns the associated records to which links will be created after saving
152
+ # the parent of the association.
153
+ def pending_creates
154
+ return [] unless objects_loaded?
155
+ objects - original_objects
156
+ end
157
+ alias_method :links, :pending_creates
158
+
159
+ # Returns the associated records to which the links will be deleted after
160
+ # saving the parent of the assocation.
161
+ def pending_deletes
162
+ # TODO: Write test for it.
163
+ return [] unless objects_loaded?
164
+ original_objects - objects
165
+ end
166
+ alias_method :unlinks, :pending_deletes
167
+
168
+ private
169
+
170
+ def load_objects
171
+ return if objects_loaded?
172
+
173
+ @objects = original_association.to_a.clone
174
+ @original_objects = @objects.clone.freeze
175
+ objects_loaded!
176
+ end
177
+
178
+ def objects_loaded?
179
+ @load_state == :loaded
180
+ end
181
+
182
+ def objects_loaded!
183
+ @load_state = :loaded
184
+ end
185
+
186
+ end
187
+ end