better-ripple 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/LICENSE +17 -0
  2. data/README.md +182 -0
  3. data/RELEASE_NOTES.md +284 -0
  4. data/better-ripple.gemspec +55 -0
  5. data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
  6. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +25 -0
  7. data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
  8. data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
  9. data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
  10. data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
  11. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  12. data/lib/rails/generators/ripple/model/templates/model.rb.erb +10 -0
  13. data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
  14. data/lib/rails/generators/ripple/observer/templates/observer.rb.erb +2 -0
  15. data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
  16. data/lib/rails/generators/ripple/test/test_generator.rb +44 -0
  17. data/lib/rails/generators/ripple_generator.rb +79 -0
  18. data/lib/ripple.rb +86 -0
  19. data/lib/ripple/associations.rb +380 -0
  20. data/lib/ripple/associations/embedded.rb +35 -0
  21. data/lib/ripple/associations/instantiators.rb +26 -0
  22. data/lib/ripple/associations/linked.rb +65 -0
  23. data/lib/ripple/associations/many.rb +38 -0
  24. data/lib/ripple/associations/many_embedded_proxy.rb +39 -0
  25. data/lib/ripple/associations/many_linked_proxy.rb +66 -0
  26. data/lib/ripple/associations/many_reference_proxy.rb +95 -0
  27. data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
  28. data/lib/ripple/associations/one.rb +20 -0
  29. data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
  30. data/lib/ripple/associations/one_key_proxy.rb +58 -0
  31. data/lib/ripple/associations/one_linked_proxy.rb +26 -0
  32. data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
  33. data/lib/ripple/associations/proxy.rb +118 -0
  34. data/lib/ripple/attribute_methods.rb +132 -0
  35. data/lib/ripple/attribute_methods/dirty.rb +59 -0
  36. data/lib/ripple/attribute_methods/query.rb +34 -0
  37. data/lib/ripple/attribute_methods/read.rb +28 -0
  38. data/lib/ripple/attribute_methods/write.rb +25 -0
  39. data/lib/ripple/callbacks.rb +71 -0
  40. data/lib/ripple/conflict/basic_resolver.rb +86 -0
  41. data/lib/ripple/conflict/document_hooks.rb +46 -0
  42. data/lib/ripple/conflict/resolver.rb +79 -0
  43. data/lib/ripple/conflict/test_helper.rb +34 -0
  44. data/lib/ripple/conversion.rb +29 -0
  45. data/lib/ripple/core_ext.rb +3 -0
  46. data/lib/ripple/core_ext/casting.rb +151 -0
  47. data/lib/ripple/core_ext/indexes.rb +89 -0
  48. data/lib/ripple/core_ext/object.rb +8 -0
  49. data/lib/ripple/document.rb +105 -0
  50. data/lib/ripple/document/bucket_access.rb +25 -0
  51. data/lib/ripple/document/finders.rb +131 -0
  52. data/lib/ripple/document/key.rb +35 -0
  53. data/lib/ripple/document/link.rb +30 -0
  54. data/lib/ripple/document/persistence.rb +130 -0
  55. data/lib/ripple/embedded_document.rb +63 -0
  56. data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
  57. data/lib/ripple/embedded_document/finders.rb +26 -0
  58. data/lib/ripple/embedded_document/persistence.rb +75 -0
  59. data/lib/ripple/i18n.rb +5 -0
  60. data/lib/ripple/indexes.rb +151 -0
  61. data/lib/ripple/inspection.rb +32 -0
  62. data/lib/ripple/locale/en.yml +26 -0
  63. data/lib/ripple/locale/fr.yml +24 -0
  64. data/lib/ripple/nested_attributes.rb +275 -0
  65. data/lib/ripple/observable.rb +28 -0
  66. data/lib/ripple/properties.rb +74 -0
  67. data/lib/ripple/property_type_mismatch.rb +12 -0
  68. data/lib/ripple/railtie.rb +26 -0
  69. data/lib/ripple/railties/ripple.rake +103 -0
  70. data/lib/ripple/serialization.rb +82 -0
  71. data/lib/ripple/test_server.rb +35 -0
  72. data/lib/ripple/timestamps.rb +25 -0
  73. data/lib/ripple/translation.rb +18 -0
  74. data/lib/ripple/validations.rb +65 -0
  75. data/lib/ripple/validations/associated_validator.rb +43 -0
  76. data/lib/ripple/version.rb +3 -0
  77. metadata +310 -0
@@ -0,0 +1,44 @@
1
+ require 'rails/generators/ripple_generator'
2
+
3
+ module Ripple
4
+ module Generators
5
+ class TestGenerator < Base
6
+ desc 'Generates test helpers for Ripple. Test::Unit, RSpec and Cucumber are supported.'
7
+ # Cucumber
8
+ def create_cucumber_file
9
+ if File.directory?("features/support")
10
+ template 'cucumber.rb.erb', 'features/support/ripple.rb'
11
+ end
12
+ end
13
+
14
+ # RSpec
15
+ def create_rspec_file
16
+ if File.file?('spec/spec_helper.rb')
17
+ rspec_prelude = /\s*R[Ss]pec\.configure do \|config\|/
18
+ indentation = File.binread('spec/spec_helper.rb').match(rspec_prelude)[0].match(/^\s*/)[0]
19
+ inject_into_file 'spec/spec_helper.rb', :before => rspec_prelude do
20
+ "#{indentation}require 'ripple/test_server'\n"
21
+ end
22
+ inject_into_file 'spec/spec_helper.rb', :after => rspec_prelude do
23
+ "\n#{indentation} config.before(:suite) { Ripple::TestServer.setup }" +
24
+ "\n#{indentation} config.after(:each) { Ripple::TestServer.clear }\n"
25
+ end
26
+ end
27
+ end
28
+
29
+ # Test::Unit
30
+ def create_test_unit_file
31
+ if File.file?('test/test_helper.rb')
32
+ test_case_prelude = /\s*class ActiveSupport::TestCase/
33
+ indentation = File.binread('test/test_helper.rb').match(test_case_prelude)[0].match(/^\s*/)[0]
34
+ inject_into_file "test/test_helper.rb", :before => test_case_prelude do
35
+ "#{indentation}# Setup in-memory test server for Riak\n#{indentation}require 'ripple/test_server'\n\n"
36
+ end
37
+ inject_into_class "test/test_helper.rb", 'ActiveSupport::TestCase' do
38
+ "#{indentation} setup { Ripple::TestServer.setup }\n#{indentation} teardown { Ripple::TestServer.clear }\n\n"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,79 @@
1
+ require 'rails/generators'
2
+ require "rails/generators/named_base"
3
+ require "rails/generators/active_model"
4
+
5
+ class RippleGenerator < ::Rails::Generators::Base
6
+ def create_ripple
7
+ invoke "ripple:configuration"
8
+ invoke "ripple:js"
9
+ invoke "ripple:test"
10
+ end
11
+ end
12
+
13
+ module Ripple
14
+ # ActiveModel generators for use in a Rails project.
15
+ module Generators
16
+ # @private
17
+ class Base < ::Rails::Generators::Base
18
+ def self.source_root
19
+ @_ripple_source_root ||=
20
+ File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
21
+ end
22
+ end
23
+
24
+ class NamedBase < ::Rails::Generators::NamedBase
25
+ def self.source_root
26
+ @_ripple_source_root ||=
27
+ File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
28
+ end
29
+ end
30
+
31
+ # Generator for a {Ripple::Document} model
32
+ class ActiveModel < ::Rails::Generators::ActiveModel
33
+ def self.all(klass)
34
+ "#{klass}.list"
35
+ end
36
+
37
+ def self.find(klass, params=nil)
38
+ "#{klass}.find(#{params})"
39
+ end
40
+
41
+ def self.build(klass, params=nil)
42
+ if params
43
+ "#{klass}.new(#{params})"
44
+ else
45
+ "#{klass}.new"
46
+ end
47
+ end
48
+
49
+ def save
50
+ "#{name}.save"
51
+ end
52
+
53
+ def update_attributes(params=nil)
54
+ "#{name}.update_attributes(#{params})"
55
+ end
56
+
57
+ def errors
58
+ "#{name}.errors"
59
+ end
60
+
61
+ def destroy
62
+ "#{name}.destroy"
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # @private
69
+ module Rails
70
+ module Generators
71
+ class GeneratedAttribute #:nodoc:
72
+ def type_class
73
+ return "Time" if type.to_s == "datetime"
74
+ return "String" if type.to_s == "text"
75
+ return type.to_s.camelcase
76
+ end
77
+ end
78
+ end
79
+ end
data/lib/ripple.rb ADDED
@@ -0,0 +1,86 @@
1
+ require 'riak'
2
+ require 'erb'
3
+ require 'yaml'
4
+ require 'active_model'
5
+ require 'ripple/core_ext'
6
+ require 'ripple/translation'
7
+ require 'ripple/document'
8
+ require 'ripple/embedded_document'
9
+
10
+ # Contains the classes and modules related to the ODM built on top of
11
+ # the basic Riak client.
12
+ module Ripple
13
+ class << self
14
+ # @return [Riak::Client] The client for the current thread.
15
+ def client
16
+ Thread.current[:ripple_client] ||= Riak::Client.new(client_config)
17
+ end
18
+
19
+ # Sets the client for the current thread.
20
+ # @param [Riak::Client] value the client
21
+ def client=(value)
22
+ Thread.current[:ripple_client] = value
23
+ end
24
+
25
+ # Sets the global Ripple configuration.
26
+ def config=(hash)
27
+ self.client = nil
28
+ @config = hash.symbolize_keys
29
+ end
30
+
31
+ # Reads the global Ripple configuration.
32
+ def config
33
+ @config ||= {}
34
+ end
35
+
36
+ # The format in which date/time objects will be serialized to
37
+ # strings in JSON. Defaults to :iso8601, and can be set in
38
+ # Ripple.config.
39
+ # @return [Symbol] the date format
40
+ def date_format
41
+ (config[:date_format] ||= :iso8601).to_sym
42
+ end
43
+
44
+ # Sets the format for date/time objects that are serialized to
45
+ # JSON.
46
+ # @param [Symbol] format the date format
47
+ def date_format=(format)
48
+ config[:date_format] = format.to_sym
49
+ end
50
+
51
+ # Loads the Ripple configuration from a given YAML file.
52
+ # Evaluates the configuration with ERB before loading.
53
+ def load_configuration(config_file, config_keys = [:ripple])
54
+ config_file = File.expand_path(config_file)
55
+ config_hash = YAML.load(ERB.new(File.read(config_file)).result).with_indifferent_access
56
+ config_keys.each {|k| config_hash = config_hash[k]}
57
+ configure_ports(config_hash)
58
+ self.config = config_hash || {}
59
+ rescue Errno::ENOENT
60
+ raise Ripple::MissingConfiguration.new(config_file)
61
+ end
62
+ alias_method :load_config, :load_configuration
63
+
64
+ private
65
+ def client_config
66
+ config.slice(*Riak::Client::VALID_OPTIONS)
67
+ end
68
+
69
+ def configure_ports(config)
70
+ return unless config && config[:min_port]
71
+ config[:http_port] ||= (config[:min_port].to_i)
72
+ config[:pb_port] ||= (config[:min_port].to_i + 1)
73
+ end
74
+ end
75
+
76
+ # Exception raised when the path passed to
77
+ # {Ripple::load_configuration} does not point to a existing file.
78
+ class MissingConfiguration < StandardError
79
+ include Translation
80
+ def initialize(file_path)
81
+ super(t("missing_configuration", :file => file_path))
82
+ end
83
+ end
84
+ end
85
+
86
+ require 'ripple/railtie' if defined? Rails::Railtie
@@ -0,0 +1,380 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/dependencies'
3
+ require 'riak/walk_spec'
4
+ require 'ripple/translation'
5
+ require 'ripple/associations/proxy'
6
+ require 'ripple/associations/instantiators'
7
+ require 'ripple/associations/linked'
8
+ require 'ripple/associations/embedded'
9
+ require 'ripple/associations/many'
10
+ require 'ripple/associations/one'
11
+ require 'ripple/associations/linked'
12
+ require 'ripple/associations/one_embedded_proxy'
13
+ require 'ripple/associations/many_embedded_proxy'
14
+ require 'ripple/associations/one_linked_proxy'
15
+ require 'ripple/associations/many_linked_proxy'
16
+ require 'ripple/associations/many_stored_key_proxy'
17
+ require 'ripple/associations/one_key_proxy'
18
+ require 'ripple/associations/one_stored_key_proxy'
19
+ require 'ripple/associations/many_reference_proxy'
20
+
21
+ module Ripple
22
+ # Adds associations via links and embedding to {Ripple::Document}
23
+ # models. Examples:
24
+ #
25
+ # # Documents can contain embedded documents, and link to other standalone documents
26
+ # # via associations using the many and one class methods.
27
+ # class Person
28
+ # include Ripple::Document
29
+ # property :name, String
30
+ # many :addresses
31
+ # many :friends, :class_name => "Person"
32
+ # one :account
33
+ # end
34
+ #
35
+ # # Account and Address are embeddable documents
36
+ # class Account
37
+ # include Ripple::EmbeddedDocument
38
+ # property :paid_until, Time
39
+ # embedded_in :person # Adds "person" method to get parent document
40
+ # end
41
+ #
42
+ # class Address
43
+ # include Ripple::EmbeddedDocument
44
+ # property :street, String
45
+ # property :city, String
46
+ # property :state, String
47
+ # property :zip, String
48
+ # end
49
+ #
50
+ # person = Person.find("adamhunter")
51
+ # person.friends << Person.find("seancribbs") # Links to people/seancribbs with tag "friend"
52
+ # person.addresses << Address.new(:street => "100 Main Street") # Adds an embedded address
53
+ # person.account.paid_until = 3.months.from_now
54
+ module Associations
55
+ extend ActiveSupport::Concern
56
+
57
+ module ClassMethods
58
+ include Translation
59
+ # @private
60
+ def inherited(subclass)
61
+ super
62
+ subclass.associations.merge!(associations)
63
+ end
64
+
65
+ # Associations defined on the document
66
+ def associations
67
+ @associations ||= {}.with_indifferent_access
68
+ end
69
+
70
+ # Associations of embedded documents
71
+ def embedded_associations
72
+ associations.values.select(&:embedded?)
73
+ end
74
+
75
+ # Associations of linked documents
76
+ def linked_associations
77
+ associations.values.select(&:linked?)
78
+ end
79
+
80
+ # Associations of stored_key documents
81
+ def stored_key_associations
82
+ associations.values.select(&:stored_key?)
83
+ end
84
+
85
+ # Creates a singular association
86
+ def one(name, options={})
87
+ configure_for_key_correspondence if options[:using] === :key
88
+ create_association(:one, name, options)
89
+ end
90
+
91
+ # Creates a plural association
92
+ def many(name, options={})
93
+ raise ArgumentError, t('many_key_association') if options[:using] === :key
94
+ create_association(:many, name, options)
95
+ end
96
+
97
+ def configure_for_key_correspondence
98
+ include Ripple::Associations::KeyDelegator
99
+ end
100
+
101
+ private
102
+ def create_association(type, name, options={})
103
+ association = associations[name] = Association.new(type, name, options)
104
+ association.validate!(self)
105
+ association.setup_on(self)
106
+
107
+ define_method(name) do
108
+ get_proxy(association)
109
+ end
110
+
111
+ define_method("#{name}=") do |value|
112
+ get_proxy(association).replace(value)
113
+ value
114
+ end
115
+
116
+ unless association.many?
117
+ define_method("#{name}?") do
118
+ get_proxy(association).present?
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+
125
+ # @private
126
+ def get_proxy(association)
127
+ unless proxy = instance_variable_get(association.ivar)
128
+ proxy = association.proxy_class.new(self, association)
129
+ instance_variable_set(association.ivar, proxy)
130
+ end
131
+ proxy
132
+ end
133
+
134
+ # @private
135
+ def reset_associations
136
+ self.class.associations.each do |name, assoc_object|
137
+ send(name).reset
138
+ end
139
+ end
140
+
141
+ # Adds embedded documents to the attributes
142
+ # @private
143
+ def attributes_for_persistence
144
+ self.class.embedded_associations.inject(super) do |attrs, association|
145
+ documents = instance_variable_get(association.ivar)
146
+ # We must explicitly check #nil? (rather than just saying `if documents`)
147
+ # because documents can be an association proxy that is proxying nil.
148
+ # In this case ruby treats documents as true because it is not _really_ nil,
149
+ # but #nil? will tell us if it is proxying nil.
150
+
151
+ unless documents.nil?
152
+ attrs[association.name] = documents.is_a?(Array) ? documents.map(&:attributes_for_persistence) : documents.attributes_for_persistence
153
+ end
154
+ attrs
155
+ end
156
+ end
157
+
158
+ def propagate_callbacks_to_embedded_associations(name, kind)
159
+ self.class.embedded_associations.each do |association|
160
+ documents = instance_variable_get(association.ivar)
161
+ # We must explicitly check #nil? (rather than just saying `if documents`)
162
+ # because documents can be an association proxy that is proxying nil.
163
+ # In this case ruby treats documents as true because it is not _really_ nil,
164
+ # but #nil? will tell us if it is proxying nil.
165
+ next if documents.nil?
166
+
167
+ Array(documents).each do |doc|
168
+ doc.send("_#{name}_callbacks").each do |callback|
169
+ next unless callback.kind == kind
170
+ doc.send(callback.filter)
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # Propagates callbacks (save/create/update/destroy) to embedded associated documents.
177
+ # This is necessary so that when a parent is saved, the embedded child's before_save
178
+ # hooks are run as well.
179
+ # @private
180
+ def run_callbacks(name, *args, &block)
181
+ # validation is already propagated to embedded documents via the
182
+ # AssociatedValidator. We don't need to duplicate the propagation here.
183
+ return super if name == :validation
184
+
185
+ propagate_callbacks_to_embedded_associations(name, :before)
186
+ return_value = super
187
+ propagate_callbacks_to_embedded_associations(name, :after)
188
+ return_value
189
+ end
190
+ end
191
+
192
+ # The "reflection" for an association - metadata about how it is
193
+ # configured.
194
+ class Association
195
+ include Ripple::Translation
196
+ attr_reader :type, :name, :options
197
+
198
+ # association options :using, :class_name, :class, :extend,
199
+ # options that may be added :validate
200
+
201
+ def initialize(type, name, options={})
202
+ @type, @name, @options = type, name, options.to_options
203
+ end
204
+
205
+ def validate!(owner)
206
+ # TODO: Refactor this into an association subclass. See also GH #284
207
+ if @options[:using] == :stored_key
208
+ single_name = ActiveSupport::Inflector.singularize(@name.to_s)
209
+ prop_name = "#{single_name}_key"
210
+ prop_name << "s" if many?
211
+ raise ArgumentError, t('stored_key_requires_property', :name => prop_name) unless owner.properties.include?(prop_name)
212
+ end
213
+ end
214
+
215
+ # @return String The class name of the associated object(s)
216
+ def class_name
217
+ @class_name ||= case
218
+ when @options[:class_name]
219
+ @options[:class_name]
220
+ when @options[:class]
221
+ @options[:class].to_s
222
+ when many?
223
+ @name.to_s.classify
224
+ else
225
+ @name.to_s.camelize
226
+ end
227
+ end
228
+
229
+ # @return [Class] The class of the associated object(s)
230
+ def klass
231
+ @klass ||= discover_class
232
+ end
233
+
234
+ # @return [true,false] Is the cardinality of the association > 1
235
+ def many?
236
+ @type == :many
237
+ end
238
+
239
+ # @return [true,false] Is the cardinality of the association == 1
240
+ def one?
241
+ @type == :one
242
+ end
243
+
244
+ # @return [true,false] Is the associated class an EmbeddedDocument
245
+ def embedded?
246
+ klass.embeddable?
247
+ end
248
+
249
+ # TODO: Polymorphic not supported
250
+ # @return [true,false] Does the association support more than one associated class
251
+ def polymorphic?
252
+ false
253
+ end
254
+
255
+ # @return [true,false] Does the association use links
256
+ def linked?
257
+ using == :linked
258
+ end
259
+
260
+ # @return [true,false] Does the association use stored_key
261
+ def stored_key?
262
+ using == :stored_key
263
+ end
264
+
265
+ # @return [String] the instance variable in the owner where the association will be stored
266
+ def ivar
267
+ "@_#{name}"
268
+ end
269
+
270
+ # @return [Class] the association proxy class
271
+ def proxy_class
272
+ @proxy_class ||= proxy_class_name.constantize
273
+ end
274
+
275
+ # @return [String] the class name of the association proxy
276
+ def proxy_class_name
277
+ klass_name = (many? ? 'Many' : 'One') + using.to_s.camelize + ('Polymorphic' if polymorphic?).to_s + 'Proxy'
278
+ "Ripple::Associations::#{klass_name}"
279
+ end
280
+
281
+ # @return [Proc] a filter proc to be used with Enumerable#select for collecting links that belong to this association (only when #linked? is true)
282
+ def link_filter
283
+ linked? ? lambda {|link| link.tag == link_tag } : lambda {|_| false }
284
+ end
285
+
286
+ # @return [String,nil] when #linked? is true, the tag for outgoing links
287
+ def link_tag
288
+ linked? ? Array(link_spec).first.tag : nil
289
+ end
290
+
291
+ def bucket_name
292
+ polymorphic? ? '_' : klass.bucket_name
293
+ end
294
+
295
+ # @return [Riak::WalkSpec] when #linked? is true, a specification for which links to follow to retrieve the associated documents
296
+ def link_spec
297
+ # TODO: support transitive linked associations
298
+ if linked?
299
+ tag = name.to_s
300
+ Riak::WalkSpec.new(:tag => tag, :bucket => bucket_name)
301
+ else
302
+ nil
303
+ end
304
+ end
305
+
306
+ # @return [Symbol] which method is used for representing the association - currently only supports :embedded and :linked
307
+ def using
308
+ @using ||= options[:using] || (embedded? ? :embedded : :linked)
309
+ end
310
+
311
+ # @raise [ArgumentError] if the value does not match the class of the association
312
+ def verify_type!(value, owner)
313
+ unless type_matches?(value)
314
+ raise ArgumentError.new(t('invalid_association_value',
315
+ :name => name,
316
+ :owner => owner.inspect,
317
+ :klass => polymorphic? ? "<polymorphic>" : klass.name,
318
+ :value => value.inspect))
319
+ end
320
+ end
321
+
322
+ # @private
323
+ def type_matches?(value)
324
+ case
325
+ when polymorphic?
326
+ one? || Array === value
327
+ when many?
328
+ Array === value && value.all? {|d| (embedded? && Hash === d) || klass === d }
329
+ when one?
330
+ value.nil? || (embedded? && Hash === value) || value.kind_of?(klass)
331
+ end
332
+ end
333
+
334
+ def uses_search?
335
+ (options[:using] == :reference)
336
+ end
337
+
338
+ def setup_on(model)
339
+ @model = model
340
+ define_callbacks_on(model)
341
+ if uses_search?
342
+ klass.before_save do |o|
343
+ unless o.class.bucket.is_indexed?
344
+ o.class.bucket.enable_index!
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+ def define_callbacks_on(klass)
351
+ _association = self
352
+
353
+ klass.before_save do
354
+ if _association.linked? && !@_in_save_loaded_documents_callback
355
+ @_in_save_loaded_documents_callback = true
356
+
357
+ begin
358
+ send(_association.name).loaded_documents.each do |document|
359
+ document.save if document.new? || document.changed?
360
+ end
361
+ ensure
362
+ remove_instance_variable(:@_in_save_loaded_documents_callback)
363
+ end
364
+ end
365
+ end
366
+ end
367
+
368
+ private
369
+ def discover_class
370
+ options[:class] || (@model && find_class(@model, class_name)) || class_name.constantize
371
+ end
372
+
373
+ def find_class(scope, class_name)
374
+ return nil if class_name.include?("::")
375
+ class_sym = class_name.to_sym
376
+ parent_scope = scope.parents.unshift(scope).find {|s| ActiveSupport::Dependencies.local_const_defined?(s, class_sym) }
377
+ parent_scope.const_get(class_sym) if parent_scope
378
+ end
379
+ end
380
+ end