nested_store_attributes 0.0.1

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