deferring 0.0.1

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