deferring 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Appraisals ADDED
@@ -0,0 +1,15 @@
1
+ appraise 'rails-30' do
2
+ gem 'activerecord', '3.0.19'
3
+ end
4
+
5
+ appraise 'rails-32' do
6
+ gem 'activerecord', '3.2.17'
7
+ end
8
+
9
+ appraise 'rails-40' do
10
+ gem 'activerecord', '4.0.4'
11
+ end
12
+
13
+ appraise 'rails-41' do
14
+ gem 'activerecord', '4.1.0'
15
+ end
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Robin Roestenburg
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # Deferring
2
+
3
+ Deferring makes it possible to delay saving ActiveRecord associations until the
4
+ parent object has been saved.
5
+
6
+ Currently supporting Rails 3.0, 3.2, 4.0 and 4.1 on Ruby 1.9.3.
7
+
8
+ It is important to note that Deferring does not touch the original `has_many`
9
+ and `has_and_belongs_to_many` associations. You can use them, without worrying
10
+ about any changed behaviour or side-effects from using Deferring.
11
+
12
+ **NOTE: This is currently work in progress.**
13
+
14
+ ## Why use it?
15
+
16
+ Let's take a look at the following example:
17
+
18
+ ``` ruby
19
+ class Person
20
+ has_and_belongs_to_many :teams
21
+ validates :name, presence: true
22
+ end
23
+
24
+ class Team
25
+ has_and_belongs_to_many :people
26
+ end
27
+
28
+ support = Team.create(name: 'Support')
29
+ person = Person.create(name: 'Bob')
30
+
31
+ person.teams << support
32
+ person.name = nil
33
+ person.save
34
+ # => false, because the name attribute is empty
35
+
36
+ Person.first.teams
37
+ # => [#<Team id: 4, name: "Support", ... ]
38
+ ```
39
+
40
+ The links to the Teams associated to the Person are stored directly, before the
41
+ (in this case invalid) parent is actually saved. This is how Rails' `has_many`
42
+ and `has_and_belongs_to_many` associations work, but not how (imho) they should
43
+ work in this situation.
44
+
45
+ The `deferring` gem will delay creating the links between Person and Team until
46
+ the Person has been saved successfully. Let's look at the example again, only
47
+ now using the `deferring` gem:
48
+
49
+ ``` ruby
50
+ class Person
51
+ deferred_has_and_belongs_to_many :teams
52
+ validates :name, presence: true
53
+ end
54
+
55
+ class Team
56
+ has_and_belongs_to_many :people
57
+ end
58
+ support = Team.create(name: 'Support')
59
+ person = Person.create(name: 'Bob')
60
+
61
+ person.teams << support
62
+ person.name = nil
63
+ person.save
64
+ # => false, because the name attribute is empty
65
+
66
+ Person.first.teams
67
+ # => []
68
+ ```
69
+
70
+
71
+ ## Use cases
72
+
73
+ * Auditing
74
+ * ...
75
+
76
+
77
+ ## Credits/Rationale
78
+
79
+ The idea for this gem was originally thought of by Tyler Rick (see [this Ruby
80
+ form thread from 2006](https://www.ruby-forum.com/topic/81095)). The gem created
81
+ by TylerRick is still
82
+ [available](https://github.com/TylerRick/has_and_belongs_to_many_with_deferred_save),
83
+ but unmaintained. This gem has been forked by Martin Koerner, who released his
84
+ fork as a gem called
85
+ [`deferred_associations`](https://rubygems.org/gems/deferred_associations).
86
+ Koerner fixes some issues with Rick's original implementation and added support
87
+ for Rails 3 and 4.
88
+
89
+ A project I am working on, uses the
90
+ [`autosave_habtm`](https://rubygems.org/gems/autosave_habtm) gem, which kind of
91
+ takes different approach to doing the same thing. This gem only supports Rails
92
+ 3.0.
93
+
94
+ As we are upgrading to Rails 3.2 (and later Rails 4), I needed a gem to provide
95
+ this behaviour. Upgrading either one of the gems would result into rewriting a
96
+ lot of the code (for different reasons, some purely esthetic :)), so that is why
97
+ I wrote a new gem.
98
+
99
+
100
+ ## Getting started
101
+
102
+ ### Installation
103
+
104
+ Add this line to your application's Gemfile:
105
+
106
+ gem 'deferring'
107
+
108
+ And then execute:
109
+
110
+ $ bundle
111
+
112
+ Or install it yourself as:
113
+
114
+ $ gem install deferring
115
+
116
+
117
+ ### How do I use it?
118
+
119
+ Deferring adds a couple of methods to your ActiveRecord models. These are:
120
+
121
+ - `deferred_has_and_belongs_to_many`
122
+ - `deferred_accepts_nested_attributes_for`
123
+ - `deferred_has_many`
124
+
125
+ These methods wrap the existing methods. For instance, `deferred_has_many` will
126
+ call `has_many` in order to set up the association.
127
+
128
+ **TODO:** Describe pending_creates/pending_deletes/links/unlinks/callbacks/
129
+ original_name/checked.
130
+
131
+
132
+ ### How does it work?
133
+
134
+ Deferring wraps the original ActiveRecord association and replaces the accessor
135
+ methods to the association by a custom object that will keep track of the
136
+ updates to the association. This wrapper is basically an array with some extras
137
+ to match the ActiveRecord API.
138
+
139
+ When the parent is saved, this object is assigned to the original association
140
+ (using an `after_save` callback on the parent model) which will automatically
141
+ save the changes to the database.
142
+
143
+ For the astute reader: Yes, the gem abuses the exact problem it is trying to
144
+ avoid ;-)
145
+
146
+ ### Gotchas
147
+
148
+ #### Using autosave (or not actually)
149
+
150
+ TL;DR; Using `autosave: true` (or false) on a deferred association will work,
151
+ but does not do anything.
152
+
153
+ This is what the Rails documentation says about the AutosaveAssociation:
154
+
155
+ _AutosaveAssociation is a module that takes care of automatically saving
156
+ associated records when their parent is saved. In addition to saving, it also
157
+ destroys any associated records that were marked for destruction._
158
+
159
+ _If validations for any of the associations fail, their error messages will be
160
+ applied to the parent._
161
+
162
+ The `deferring` gem works with `pending_deletes` (or the alias `unlinks`)
163
+ instead of the `marked_for_destruction` flag, so everything related to that in
164
+ AutosaveAssociation does not work as you would expect.
165
+
166
+ Also, `deferring` adds the associated records present in a deferred
167
+ association to the original (in this case, autosaved) association by assigning
168
+ the array of associated records to original association. This kind of assignment
169
+ bypasses the autosave behaviour, see the _Why use it?_ part on top of this
170
+ README.
171
+
172
+ #### Using custom callback methods
173
+
174
+ **TODO**: This is incorrect and has to be rewritten to match code.
175
+
176
+ You can use custom callback functions. However, the callbacks for defferred
177
+ associations are triggered at a different point in time.
178
+
179
+ An example to illustrate:
180
+
181
+ ``` ruby
182
+ class Person < ActiveRecord::Base
183
+ has_and_belongs_to_many :teams, before_add: :before_adding
184
+ deferred_has_and_belongs_to_many :pets, before_add: :before_adding
185
+
186
+ def audit_log
187
+ @log = []
188
+ end
189
+
190
+ def before_adding(record)
191
+ audit_log << "Before adding #{record.class} with id #{record.id}"
192
+ end
193
+ end
194
+ ```
195
+
196
+ This sets up a Person model that has a regular HABTM association with teams and
197
+ that has a deferred HABTM association with pets. Each time a team or pet is
198
+ added to the database a log statement is written to the audit log (using the
199
+ `before_adding` callback function).
200
+
201
+ The regular HABTM association behaves likes this:
202
+
203
+ ``` ruby
204
+ person = Person.first
205
+ person.teams << Team.find(1)
206
+ person.audit_log # => ['Before adding Team 1']
207
+ ```
208
+
209
+ As records of deferred associations are saved to the database after saving the
210
+ parent the behavior is a bit different:
211
+
212
+ ``` ruby
213
+ person = Person.first
214
+ person.pets << Pet.find(1)
215
+ person.audit_log # => []
216
+
217
+ person.save
218
+ person.audit_log # => ['Before adding Pet 1']
219
+ ```
220
+
221
+ ## TODO
222
+
223
+ * add support for more Rubies
224
+ * check out what is going on with uniq: true
225
+ * collection(true) (same as reload)
226
+ * collection.replace
227
+ * validations!
228
+ * validate: false does not work
229
+
230
+ ## Contributing
231
+
232
+ 1. Fork it
233
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
234
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
235
+ 4. Push to the branch (`git push origin my-new-feature`)
236
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'bundler/gem_tasks'
5
+
6
+ require 'rspec/core/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ desc 'Run the specs.'
11
+ task default: :spec
data/deferring.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'deferring/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'deferring'
8
+ spec.version = Deferring::VERSION
9
+ spec.authors = ['Robin Roestenburg']
10
+ spec.email = ['robin@roestenburg.io']
11
+ spec.description = %q{
12
+ The Deferring gem makes it possible to defer saving ActiveRecord
13
+ associations until the parent object is saved.
14
+ }
15
+ spec.summary = %q{Defer saving ActiveRecord associations until parent is saved}
16
+ spec.homepage = 'http://github.com/robinroestenburg/delay_many'
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files`.split($/)
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'activerecord', '> 3.0'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.3'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'sqlite3'
30
+ spec.add_development_dependency 'appraisal'
31
+ end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "3.0.19"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,51 @@
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 (3.0.19)
11
+ activesupport (= 3.0.19)
12
+ builder (~> 2.1.2)
13
+ i18n (~> 0.5.0)
14
+ activerecord (3.0.19)
15
+ activemodel (= 3.0.19)
16
+ activesupport (= 3.0.19)
17
+ arel (~> 2.0.10)
18
+ tzinfo (~> 0.3.23)
19
+ activesupport (3.0.19)
20
+ appraisal (1.0.0)
21
+ bundler
22
+ rake
23
+ thor (>= 0.14.0)
24
+ arel (2.0.10)
25
+ builder (2.1.2)
26
+ diff-lcs (1.2.5)
27
+ i18n (0.5.2)
28
+ rake (10.3.1)
29
+ rspec (2.14.1)
30
+ rspec-core (~> 2.14.0)
31
+ rspec-expectations (~> 2.14.0)
32
+ rspec-mocks (~> 2.14.0)
33
+ rspec-core (2.14.8)
34
+ rspec-expectations (2.14.5)
35
+ diff-lcs (>= 1.1.3, < 2.0)
36
+ rspec-mocks (2.14.6)
37
+ sqlite3 (1.3.9)
38
+ thor (0.19.1)
39
+ tzinfo (0.3.39)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ activerecord (= 3.0.19)
46
+ appraisal
47
+ bundler (~> 1.3)
48
+ deferring!
49
+ rake
50
+ rspec
51
+ sqlite3
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "3.2.17"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,53 @@
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 (3.2.17)
11
+ activesupport (= 3.2.17)
12
+ builder (~> 3.0.0)
13
+ activerecord (3.2.17)
14
+ activemodel (= 3.2.17)
15
+ activesupport (= 3.2.17)
16
+ arel (~> 3.0.2)
17
+ tzinfo (~> 0.3.29)
18
+ activesupport (3.2.17)
19
+ i18n (~> 0.6, >= 0.6.4)
20
+ multi_json (~> 1.0)
21
+ appraisal (1.0.0)
22
+ bundler
23
+ rake
24
+ thor (>= 0.14.0)
25
+ arel (3.0.3)
26
+ builder (3.0.4)
27
+ diff-lcs (1.2.5)
28
+ i18n (0.6.9)
29
+ multi_json (1.9.2)
30
+ rake (10.3.1)
31
+ rspec (2.14.1)
32
+ rspec-core (~> 2.14.0)
33
+ rspec-expectations (~> 2.14.0)
34
+ rspec-mocks (~> 2.14.0)
35
+ rspec-core (2.14.8)
36
+ rspec-expectations (2.14.5)
37
+ diff-lcs (>= 1.1.3, < 2.0)
38
+ rspec-mocks (2.14.6)
39
+ sqlite3 (1.3.9)
40
+ thor (0.19.1)
41
+ tzinfo (0.3.39)
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ activerecord (= 3.2.17)
48
+ appraisal
49
+ bundler (~> 1.3)
50
+ deferring!
51
+ rake
52
+ rspec
53
+ sqlite3