nested_store_attributes 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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +32 -0
  5. data/lib/nested_store_attributes.rb +3 -0
  6. data/lib/nested_store_attributes/accepts_store_attributes.rb +248 -0
  7. data/lib/nested_store_attributes/version.rb +3 -0
  8. data/lib/tasks/nested_json_attributes_tasks.rake +4 -0
  9. data/test/dummy/README.rdoc +28 -0
  10. data/test/dummy/Rakefile +6 -0
  11. data/test/dummy/app/assets/javascripts/application.js +13 -0
  12. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  13. data/test/dummy/app/controllers/application_controller.rb +5 -0
  14. data/test/dummy/app/helpers/application_helper.rb +2 -0
  15. data/test/dummy/app/models/person.rb +9 -0
  16. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  17. data/test/dummy/bin/bundle +3 -0
  18. data/test/dummy/bin/rails +4 -0
  19. data/test/dummy/bin/rake +4 -0
  20. data/test/dummy/config.ru +4 -0
  21. data/test/dummy/config/application.rb +23 -0
  22. data/test/dummy/config/boot.rb +5 -0
  23. data/test/dummy/config/database.yml +25 -0
  24. data/test/dummy/config/environment.rb +5 -0
  25. data/test/dummy/config/environments/development.rb +37 -0
  26. data/test/dummy/config/environments/production.rb +78 -0
  27. data/test/dummy/config/environments/test.rb +39 -0
  28. data/test/dummy/config/initializers/assets.rb +8 -0
  29. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  30. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  31. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  32. data/test/dummy/config/initializers/inflections.rb +16 -0
  33. data/test/dummy/config/initializers/mime_types.rb +4 -0
  34. data/test/dummy/config/initializers/session_store.rb +3 -0
  35. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  36. data/test/dummy/config/locales/en.yml +23 -0
  37. data/test/dummy/config/routes.rb +56 -0
  38. data/test/dummy/config/secrets.yml +22 -0
  39. data/test/dummy/db/development.sqlite3 +0 -0
  40. data/test/dummy/db/migrate/20141016114846_create_people.rb +11 -0
  41. data/test/dummy/db/schema.rb +24 -0
  42. data/test/dummy/db/test.sqlite3 +0 -0
  43. data/test/dummy/log/development.log +16 -0
  44. data/test/dummy/log/test.log +1184 -0
  45. data/test/dummy/public/404.html +67 -0
  46. data/test/dummy/public/422.html +67 -0
  47. data/test/dummy/public/500.html +66 -0
  48. data/test/dummy/public/favicon.ico +0 -0
  49. data/test/dummy/test/fixtures/people.yml +11 -0
  50. data/test/dummy/test/models/person_test.rb +7 -0
  51. data/test/nested_json_attributes_test.rb +70 -0
  52. data/test/test_helper.rb +15 -0
  53. metadata +170 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: 77ec0ac6acc18a674c3943d7e3aff59c10e8f8b2
4
+ data.tar.gz: 23c9447ceca1d84c635e3dcc59d05a58c8387ba0
5
+ SHA512:
6
+ metadata.gz: 24efa0ff30e400a89957e6fb79134fe6320da481c0ba0930226d059f85fa3f548015318889db5816881278dbd3411ea707ac6772260a9146d8bfb569c739e92d
7
+ data.tar.gz: c2395cbe5c420d0a905994578dbacdb137c5973ae885a5d4beb590f9ca243bc4cb63b0b6213abb885b2078a36398aeca480f3b4c593fb45ef04a112a3c565bec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = NestedStoreAttributes
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'NestedStoreAttributes'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rake/testtask'
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+
32
+ task default: :test
@@ -0,0 +1,3 @@
1
+ require 'nested_store_attributes/accepts_store_attributes'
2
+ module NestedStoreAttributes
3
+ end
@@ -0,0 +1,248 @@
1
+ require 'active_support/core_ext/hash/except'
2
+ require 'active_support/core_ext/object/try'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ module NestedStoreAttributes
6
+ module AcceptsStoreAttributes #:nodoc:
7
+ class TooManyRecords < ActiveRecord::ActiveRecordError
8
+ end
9
+
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ class_attribute :store_attributes_options, instance_writer: false
14
+ self.store_attributes_options = {}
15
+ end
16
+
17
+ module ClassMethods
18
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
19
+
20
+ # Defines an attributes writer for the specified serialized attribute(s).
21
+ #
22
+ # Supported options:
23
+ # [:allow_destroy]
24
+ # If true, destroys any members from the attributes hash with a
25
+ # <tt>_destroy</tt> key and a value that evaluates to +true+
26
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
27
+ # [:reject_if]
28
+ # Allows you to specify a Proc or a Symbol pointing to a method
29
+ # that checks whether a record should be built for a certain attribute
30
+ # hash. The hash is passed to the supplied Proc or the method
31
+ # and it should return either +true+ or +false+. When no :reject_if
32
+ # is specified, a record will be built for all attribute hashes that
33
+ # do not have a <tt>_destroy</tt> value that evaluates to true.
34
+ # Passing <tt>:all_blank</tt> instead of a Proc will create a proc
35
+ # that will reject a record where all the attributes are blank excluding
36
+ # any value for _destroy.
37
+ # [:limit]
38
+ # Allows you to specify the maximum number of the nested records that
39
+ # can be processed with the nested attributes. Limit also can be specified as a
40
+ # Proc or a Symbol pointing to a method that should return number. If the size of the
41
+ # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
42
+ # exception is raised. If omitted, any number nested records can be processed.
43
+ # [:primary_key]
44
+ # Allows you to specify the primary key to use when checking for existing objects. This
45
+ # defaults to :id.
46
+ #
47
+ # Examples:
48
+ # # creates subscribers_attributes=
49
+ # accepts_store_attributes_for :subscribers, primary_key: :email
50
+ # # creates books_attributes=
51
+ # accepts_store_attributes_for :books, reject_if: proc { |attributes| attributes['name'].blank? }
52
+ # # creates books_attributes=
53
+ # accepts_store_attributes_for :books, reject_if: :all_blank
54
+ # # creates books_attributes= and posts_attributes=
55
+ # accepts_store_attributes_for :books, :posts, allow_destroy: true
56
+ def accepts_store_attributes_for(*attr_names)
57
+ options = { :allow_destroy => false, :update_only => false, :primary_key => :id }
58
+ options.update(attr_names.extract_options!)
59
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only, :primary_key)
60
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
61
+
62
+ attr_names.each do |attribute_name|
63
+ if self.attribute_names.include?(attribute_name.to_s)
64
+
65
+ store_attributes_options = self.store_attributes_options.dup
66
+ store_attributes_options[attribute_name] = options
67
+ self.store_attributes_options = store_attributes_options
68
+
69
+ store_generate_collection_writer(attribute_name)
70
+ else
71
+ raise ArgumentError, "No column found for name `#{attribute_name}'. Has it been added yet?"
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ # Generates a writer method for this attribute. Serves as a point for
79
+ # accessing the hashes in the attribute. For example, this method
80
+ # could generate the following:
81
+ #
82
+ # def pirate_attributes=(attributes)
83
+ # store_assign_nested_attributes_for_collection_association(:pirate, attributes)
84
+ # end
85
+ #
86
+ # This redirects the attempts to write objects in an association through
87
+ # the helper methods defined below. Makes it seem like the nested
88
+ # associations are just regular associations.
89
+ def store_generate_collection_writer(attribute_name)
90
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
91
+ if method_defined?(:#{attribute_name}_attributes=)
92
+ remove_method(:#{attribute_name}_attributes=)
93
+ end
94
+ def #{attribute_name}_attributes=(attributes)
95
+ store_assign_nested_attributes_for_collection(:#{attribute_name}, attributes)
96
+ end
97
+ eoruby
98
+ end
99
+ end
100
+
101
+ def _destroy
102
+ nil?
103
+ end
104
+
105
+ private
106
+
107
+ # Attribute hash keys that should not be assigned as normal attributes.
108
+ # These hash keys are nested attributes implementation details.
109
+ UNASSIGNABLE_KEYS = %w( id _destroy )
110
+
111
+
112
+ # Assigns the given attributes to the collection attribute.
113
+ #
114
+ # Hashes with an primary_key (by default <tt>:id</tt>) value matching an existing nested record
115
+ # will update that record. Hashes without an primary_key value will build
116
+ # a new record for the association. Hashes with a matching primary_key
117
+ # value and a <tt>:_destroy</tt> key set to a truthy value will mark the
118
+ # matched record for destruction.
119
+ #
120
+ # For example:
121
+ #
122
+ # store_assign_nested_attributes_for_collection_association(:people, {
123
+ # '1' => { id: '1', name: 'Peter' },
124
+ # '2' => { name: 'John' },
125
+ # '3' => { id: '2', _destroy: true }
126
+ # })
127
+ #
128
+ # Will update the name of the Person with ID 1, add a new record for
129
+ # person with the name 'John', and remove the Person with ID 2.
130
+ #
131
+ # Also accepts an Array of attribute hashes:
132
+ #
133
+ # store_assign_nested_attributes_for_collection_association(:people, [
134
+ # { id: '1', name: 'Peter' },
135
+ # { name: 'John' },
136
+ # { id: '2', _destroy: true }
137
+ # ])
138
+ def store_assign_nested_attributes_for_collection(attribute_name, attributes_collection)
139
+ options = self.store_attributes_options[attribute_name]
140
+
141
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
142
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
143
+ end
144
+
145
+ store_check_record_limit!(options[:limit], attributes_collection)
146
+ primary_key = options[:primary_key].to_s
147
+ new_collection = []
148
+
149
+ if attributes_collection.is_a? Hash
150
+ keys = attributes_collection.keys
151
+ attributes_collection = if keys.include?(primary_key) || keys.include?(primary_key.to_sym)
152
+ [attributes_collection]
153
+ else
154
+ attributes_collection.values
155
+ end
156
+ end
157
+
158
+ existing_records = self.send(attribute_name) || []
159
+ existing_records.map!(&:with_indifferent_access)
160
+
161
+ attributes_collection.each do |attributes|
162
+ attributes = attributes.with_indifferent_access
163
+
164
+ if attributes[primary_key].present? && existing_record = existing_records.delete_at(existing_records.index { |record| record[primary_key].to_s == attributes[primary_key].to_s } || existing_records.length)
165
+ unless store_call_reject_if(attribute_name, attributes)
166
+ new_collection << store_add_or_destroy(existing_record, attributes, options[:allow_destroy])
167
+ end
168
+ else
169
+ unless store_reject_new_record?(attribute_name, attributes)
170
+ new_collection << attributes.except(*UNASSIGNABLE_KEYS)
171
+ end
172
+ end
173
+ end
174
+
175
+ new_collection += existing_records
176
+ new_collection.reject!(&:nil?)
177
+ self.assign_attributes(attribute_name => new_collection)
178
+ end
179
+
180
+ # Takes in a limit and checks if the attributes_collection has too many
181
+ # records. It accepts limit in the form of symbol, proc, or
182
+ # number-like object (anything that can be compared with an integer).
183
+ #
184
+ # Raises TooManyRecords error if the attributes_collection is
185
+ # larger than the limit.
186
+ def store_check_record_limit!(limit, attributes_collection)
187
+ if limit
188
+ limit = case limit
189
+ when Symbol
190
+ send(limit)
191
+ when Proc
192
+ limit.call
193
+ else
194
+ limit
195
+ end
196
+
197
+ if limit && attributes_collection.size > limit
198
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
199
+ end
200
+ end
201
+ end
202
+
203
+ # Updates a record with the +attributes+ or returns nil if
204
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
205
+ def store_add_or_destroy(hash, attributes, allow_destroy)
206
+ hash.merge!(attributes.except(*UNASSIGNABLE_KEYS))
207
+ unless store_has_destroy_flag?(attributes) && allow_destroy
208
+ return hash
209
+ else
210
+ return nil
211
+ end
212
+ end
213
+
214
+ # Determines if a hash contains a truthy _destroy key.
215
+ def store_has_destroy_flag?(hash)
216
+ hash.stringify_keys!
217
+ ActiveRecord::ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
218
+ end
219
+
220
+ # Determines if a new record should be rejected by checking
221
+ # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
222
+ # attribute and evaluates to +true+.
223
+ def store_reject_new_record?(attribute_name, attributes)
224
+ store_has_destroy_flag?(attributes) || store_call_reject_if(attribute_name, attributes)
225
+ end
226
+
227
+ # Determines if a record with the particular +attributes+ should be
228
+ # rejected by calling the reject_if Symbol or Proc (if defined).
229
+ # The reject_if option is defined by +accepts_nested_attributes_for+.
230
+ #
231
+ # Returns false if there is a +destroy_flag+ on the attributes.
232
+ def store_call_reject_if(attribute_name, attributes)
233
+ return false if store_has_destroy_flag?(attributes)
234
+ case callback = self.store_attributes_options[attribute_name][:reject_if]
235
+ when Symbol
236
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
237
+ when Proc
238
+ callback.call(attributes)
239
+ end
240
+ end
241
+
242
+ def store_raise_nested_attributes_record_not_found!(attribute_name, record_id)
243
+ raise RecordNotFound, "Couldn't find #{attribute_name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
244
+ end
245
+ end
246
+ end
247
+
248
+ ActiveRecord::Base.send :include, NestedStoreAttributes::AcceptsStoreAttributes
@@ -0,0 +1,3 @@
1
+ module NestedStoreAttributes
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :store_attributes do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Prevent CSRF attacks by raising an exception.
3
+ # For APIs, you may want to use :null_session instead.
4
+ protect_from_forgery with: :exception
5
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,9 @@
1
+ class Person < ActiveRecord::Base
2
+
3
+ serialize :books, JSON
4
+ serialize :cars, JSON
5
+
6
+ accepts_store_attributes_for :books, primary_key: :isbn, allow_destroy: true
7
+
8
+ accepts_store_attributes_for :cars
9
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6
+ <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')