accepts_nested_ids 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 17cafe27c546d2cba99337b3fe75a190805c048a
4
+ data.tar.gz: ccc40cfda5235a83e9ecc0cc036e5a01d809462b
5
+ SHA512:
6
+ metadata.gz: fb214073171ad689b195d39c159eed73555ed42080a84726b53863790a6e535a4f8c666605884dc802b76b25a388ea36bda61b01e9d7bb39e35cd311f3cf6ee0
7
+ data.tar.gz: c02d8406d6aa4dc8ce0f4e532b9cd9799b66a7c1f5c1556773c31397c9808a089f6fcc2b8ac3c78a4c050d2cdcb4eb5a954e2c7727bbbf959cbed3d8499381c8
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ before_install: gem install bundler -v 1.10.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in accepts_nested_ids.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Little Blimp Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # AcceptsNestedIds
2
+
3
+ ## Conundrum:
4
+
5
+ You want to save associations (`has_many` or `has_many :through`) by ID, but Rails is totally not doing what you expect it to.
6
+
7
+ ## Solution:
8
+
9
+ This gem.
10
+
11
+ ## What the hell am I talking about?
12
+
13
+ Let's take a typical scenario.
14
+
15
+ ```ruby
16
+ class Project < ActiveRecord::Base
17
+ has_many :project_users
18
+ has_many :users, through: :project_users
19
+ validates :name, presence: true
20
+ end
21
+
22
+ class User < ActiveRecord::Base
23
+ has_many :project_users
24
+ has_many :projects, through: :project_users
25
+ end
26
+ ```
27
+
28
+ When creating or updating a Project, you can select a list of User IDs from a select list. Great.
29
+
30
+ ```ruby
31
+ project = Project.first
32
+ project.name = ""
33
+ project.user_ids = [1,2,3]
34
+ project.save!
35
+ ```
36
+
37
+ Oh shit.
38
+
39
+ Thats right: Rails went ahead and associated those Users, even through the save failed. Because it didn't even wait until the `save!` to associate them. It happened right here: `project.user_ids = [1,2,3]`
40
+
41
+ No one wants this. But it happens all the time.
42
+
43
+ ## Get to the point already
44
+
45
+ AcceptsNestedIds defers the saving of ID-based associations to a model's `after_save` callback. In the example above, no User associations would have been created when using this gem.
46
+
47
+ ## Bonus
48
+
49
+ Ever need audit trail functionality? Its easy, using ActiveModel::Dirty and its related methods (`changes?`, etc). However, what you won't get out-of-the-box is dirty tracking for associated attributes. Because why would you?
50
+
51
+ AcceptsNestedIds adds dirty tracking for ID-based associations:
52
+
53
+ ```ruby
54
+ project = Project.first
55
+ project.user_ids = [1,2,3]
56
+ project.changes # => "user_ids" => [[], [1,2,3]]
57
+ ```
58
+
59
+ Beauty.
60
+
61
+ ## Installation
62
+
63
+ Add this line to your application's Gemfile:
64
+
65
+ ```ruby
66
+ gem 'accepts_nested_ids'
67
+ ```
68
+
69
+ And then execute:
70
+
71
+ $ bundle
72
+
73
+ Or install it yourself as:
74
+
75
+ $ gem install accepts_nested_ids
76
+
77
+ ## Usage
78
+
79
+ ### When your association is conventionally named:
80
+
81
+ ```ruby
82
+ class Project < ActiveRecord::Base
83
+ include AcceptsNestedIds
84
+ has_many :project_users
85
+ has_many :users, through: :project_users
86
+ accepts_nested_ids_for :users
87
+ end
88
+ ```
89
+
90
+ ### When your association has a custom name:
91
+
92
+ ```ruby
93
+ class Project < ActiveRecord::Base
94
+ include AcceptsNestedIds
95
+ has_many :project_users
96
+ has_many :included_users, through: :project_users, source: :user
97
+ accepts_nested_ids_for included_users: "User"
98
+ end
99
+ ```
100
+
101
+ ### Mix and match as desired:
102
+
103
+ ```ruby
104
+ class Project < ActiveRecord::Base
105
+ include AcceptsNestedIds
106
+ has_many :documents
107
+ has_many :project_users
108
+ has_many :included_users, through: :project_users, source: :user
109
+ accepts_nested_ids_for :documents, included_users: "User"
110
+ end
111
+ ```
112
+
113
+ You can now comfortably set `document_ids` or `user_ids` on a `Project` without making a mess of things.
114
+
115
+ ## Development
116
+
117
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
118
+
119
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome on GitHub at https://github.com/uberllama/accepts_nested_ids.
124
+
125
+
126
+ ## License
127
+
128
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
129
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'accepts_nested_ids/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "accepts_nested_ids"
8
+ spec.version = AcceptsNestedIds::VERSION
9
+ spec.authors = ["Yuval Kordov", "Little Blimp"]
10
+ spec.email = ["yuval@littleblimp.com"]
11
+ spec.summary = "Predictable nesting of associations via ID"
12
+ spec.description = "Defers saving of nested associations by ID, and adds dirty tracking of said associations to parent model"
13
+ spec.homepage = "http://github.com/uberllama/accepts_nested_ids"
14
+ spec.license = "MIT"
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+ spec.add_dependency "activesupport", ">= 3.0.0"
20
+ spec.add_development_dependency "activerecord", ">= 4.2.3"
21
+ spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "sqlite3"
25
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "accepts_nested_ids"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module AcceptsNestedIds
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,135 @@
1
+ require "accepts_nested_ids/version"
2
+ require "active_support/concern"
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ # Allows a model to accept nested association IDs as an attribute.
6
+ # Normally these associations would be immediately saved, even if
7
+ # the parent model transaction failed. This class defers saving to
8
+ # after_save, and also provides dirty attribute tracking on the
9
+ # parent model.
10
+ #
11
+ module AcceptsNestedIds
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ after_save :save_nested_id_associations
16
+ end
17
+
18
+ # Defered association setter
19
+ #
20
+ # @example Method definition
21
+ # if @document_ids
22
+ # self.documents = Document.where(id: document_ids)
23
+ # end
24
+ #
25
+ def save_nested_id_associations
26
+ self.class.nested_id_associations.each do |nested_id_association|
27
+ if instance_variable_get("@#{nested_id_association.ids_attr}")
28
+ association_class = nested_id_association.class_name.constantize
29
+ ids = send(nested_id_association.ids_attr)
30
+ send("#{nested_id_association.attr}=", association_class.where(id: ids))
31
+ end
32
+ end
33
+ end
34
+
35
+ module ClassMethods
36
+
37
+ # Simple object that contains information about a nested ID association
38
+ #
39
+ # @param attr [Symbol] association attribute (ex: :documents)
40
+ # @param ids_attr [String] ids association attribute (ex: 'document_ids')
41
+ # @param class_name [String] association class name (ex: 'Document')
42
+ class NestedIdAssociation < Struct.new(:attr, :ids_attr, :class_name); end
43
+
44
+ # Sets up defered save and dirty tracking for the specified associations
45
+ #
46
+ # @example When class_name can be inferred from association name
47
+ # include AcceptsNestedIds
48
+ # accepts_nested_ids_for :documents, :carrier_companies
49
+ #
50
+ # @example When class_name is different from association name
51
+ # include AcceptsNestedIds
52
+ # accepts_nested_ids_for :documents, included_carrier_companies: "CarrierCompany"
53
+ #
54
+ # @param args [Array]
55
+ def accepts_nested_ids_for(*args)
56
+ @_nested_id_associations = map_nested_id_associations(*args)
57
+
58
+ nested_id_associations.each do |nested_id_association|
59
+
60
+ # Define ids_attr getter
61
+ #
62
+ # @example Method definition
63
+ # def document_ids
64
+ # @document_ids || (documents.loaded? ? documents.map(&:id) : documents.pluck(:id))
65
+ # end
66
+ #
67
+ define_method("#{nested_id_association.ids_attr}") do
68
+ association = send(nested_id_association.attr)
69
+ instance_variable_get("@#{nested_id_association.ids_attr}") ||
70
+ (association.loaded? ? association.map(&:id) : association.pluck(:id))
71
+ end
72
+
73
+ # Define ids_attr setter
74
+ #
75
+ # @example Method definition
76
+ # def document_ids=(value)
77
+ # return if document_ids == value
78
+ # attribute_will_change!('document_ids')
79
+ # @document_ids = value
80
+ # end
81
+ #
82
+ define_method("#{nested_id_association.ids_attr}=") do |value|
83
+ return if send(nested_id_association.ids_attr) == value
84
+ attribute_will_change!(nested_id_association.ids_attr)
85
+ instance_variable_set("@#{nested_id_association.ids_attr}", value)
86
+ end
87
+
88
+ end
89
+ end
90
+
91
+ def nested_id_associations
92
+ @_nested_id_associations
93
+ end
94
+
95
+ private
96
+
97
+ # Map module args into array of NestedIdAssociation objects with supporting properties
98
+ #
99
+ # @example
100
+ # accepts_nested_ids_for :documents, included_carrier_companies: "CarrierCompany"
101
+ # =>
102
+ # [
103
+ # { attr: :documents:, ids_attr: "document_ids", class_name: "Document"},
104
+ # { attr: :included_carrier_companies, ids_attr: "included_carrier_company_ids", class_name: "CarrierCompany" }
105
+ # ]
106
+ #
107
+ # @param args [Array]
108
+ # @return [Array]
109
+ def map_nested_id_associations(*args)
110
+ args.inject([]) do |array, arg|
111
+ if arg.is_a?(Hash)
112
+ attr = arg.keys.first
113
+ ids_attr = get_ids_attr(attr)
114
+ class_name = arg[attr]
115
+ else
116
+ attr = arg
117
+ ids_attr = get_ids_attr(attr)
118
+ class_name = arg.to_s.classify
119
+ end
120
+ array << NestedIdAssociation.new(attr, ids_attr, class_name)
121
+ array
122
+ end
123
+ end
124
+
125
+ # @example
126
+ # get_ids_attr(:documents) => "document_ids"
127
+ #
128
+ # @param attr [Symbol] Association attribute name
129
+ # @return [String]
130
+ def get_ids_attr(attr)
131
+ "#{attr.to_s.singularize}_ids"
132
+ end
133
+
134
+ end
135
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: accepts_nested_ids
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yuval Kordov
8
+ - Little Blimp
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2015-07-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 3.0.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 3.0.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: activerecord
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 4.2.3
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 4.2.3
42
+ - !ruby/object:Gem::Dependency
43
+ name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.10'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.10'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '10.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '10.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rspec
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: sqlite3
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ description: Defers saving of nested associations by ID, and adds dirty tracking of
99
+ said associations to parent model
100
+ email:
101
+ - yuval@littleblimp.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - ".travis.yml"
109
+ - Gemfile
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - accepts_nested_ids.gemspec
114
+ - bin/console
115
+ - bin/setup
116
+ - lib/accepts_nested_ids.rb
117
+ - lib/accepts_nested_ids/version.rb
118
+ homepage: http://github.com/uberllama/accepts_nested_ids
119
+ licenses:
120
+ - MIT
121
+ metadata: {}
122
+ post_install_message:
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 2.4.6
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: Predictable nesting of associations via ID
142
+ test_files: []