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.
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