Skip to content
This repository was archived by the owner on Oct 19, 2018. It is now read-only.

Commit a6d0b2a

Browse files
committed
added handling of non AR aggregates
1 parent 1ff18b2 commit a6d0b2a

File tree

16 files changed

+233
-62
lines changed

16 files changed

+233
-62
lines changed

lib/reactive_record/active_record/aggregations.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@ class AggregationReflection
1919
attr_reader :klass_name
2020
attr_reader :attribute
2121
attr_reader :mapped_attributes
22+
attr_reader :constructor
23+
24+
def construct(args)
25+
26+
end
2227

2328
def initialize(owner_class, macro, name, options = {})
2429
owner_class.reflect_on_all_aggregations << self
2530
@owner_class = owner_class
31+
@constructor = options[:constructor] || :new
2632
@klass_name = options[:class_name] || name.camelize
2733
@attribute = name
2834
if options[:mapping].respond_to? :collect
@@ -37,6 +43,24 @@ def klass
3743
@klass ||= Object.const_get(@klass_name)
3844
end
3945

46+
def serialize(object)
47+
if object.nil?
48+
object # return dummy value if that is what we got
49+
else
50+
@mapped_attributes.collect { |attr| object.send(attr) }
51+
end
52+
end
53+
54+
def deserialize(array)
55+
if array.nil?
56+
array # return dummy value if that is what we got
57+
elsif @constructor.respond_to?(:call)
58+
@constructor.call(*array)
59+
else
60+
klass.send(@constructor, *array)
61+
end
62+
end
63+
4064
end
4165

4266
end

lib/reactive_record/active_record/instance_methods.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def attributes
99
end
1010

1111
def initialize(hash = {})
12+
1213
if hash.is_a? ReactiveRecord::Base
1314
@backing_record = hash
1415
else

lib/reactive_record/active_record/reactive_record/base.rb

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class Base
3434
attr_accessor :aggregate_attribute
3535
attr_accessor :destroyed
3636
attr_accessor :updated_during
37+
attr_accessor :synced_attributes
3738

3839
# While data is being loaded from the server certain internal behaviors need to change
3940
# for example records all record changes are synced as they happen.
@@ -204,10 +205,12 @@ def reactive_set!(attribute, value)
204205
attributes[attribute].attributes[inverse_of] = nil
205206
end
206207
end
207-
elsif aggregation = @model.reflect_on_aggregation(attribute)
208+
elsif aggregation = @model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
208209

209-
unless attributes[attribute]
210-
raise "unitialized aggregate attribute - should never happen"
210+
if new?
211+
attributes[attribute] ||= aggregation.klass.new
212+
elsif !attributes[attribute]
213+
raise "uninitialized aggregate attribute - should never happen"
211214
end
212215

213216
aggregate_record = attributes[attribute].backing_record
@@ -229,7 +232,13 @@ def reactive_set!(attribute, value)
229232

230233
def update_attribute(attribute, *args)
231234
value = args[0]
232-
@synced_attributes[attribute] = value if args.count != 0 and data_loading?
235+
if args.count != 0 and data_loading?
236+
if aggregation = model.reflect_on_aggregation(attribute) and !(aggregation.klass < ActiveRecord::Base)
237+
@synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
238+
else
239+
@synced_attributes[attribute] = value
240+
end
241+
end
233242
if @virgin
234243
attributes[attribute] = value if args.count != 0
235244
return
@@ -256,11 +265,11 @@ def update_attribute(attribute, *args)
256265
if !data_loading?
257266
React::State.set_state(self, attribute, value)
258267
elsif on_opal_client? and current_value.loaded? and current_value != value # this is to handle changes in already loaded server side methods
259-
puts "not expecting to get here #{attribute}"
260-
after(0.001) { React::State.set_state(self, attribute, value) }
268+
#after(0.001) { React::State.set_state(self, attribute, value) }
269+
React::State.set_state(self, attribute, value, true)
261270
end
262271
if empty_before != changed_attributes.empty?
263-
React::State.set_state(self, "!CHANGED!", !changed_attributes.empty?) unless on_opal_server? or data_loading?
272+
React::State.set_state(self, "!CHANGED!", !changed_attributes.empty?, true) unless on_opal_server? or data_loading?
264273
aggregate_owner.update_attribute(aggregate_attribute) if aggregate_owner
265274
end
266275
end
@@ -281,16 +290,8 @@ def errors
281290

282291
def sync!(hash = {}) # does NOT notify (see saved! for notification)
283292
@attributes.merge! hash
284-
@synced_attributes = @attributes.dup
285-
@synced_attributes.each do |key, value|
286-
if value.is_a? Collection
287-
@synced_attributes[key] = value.dup_for_sync
288-
elsif aggregation = model.reflect_on_aggregation(key)
289-
value.backing_record.sync!
290-
elsif !model.reflect_on_association(key)
291-
@synced_attributes[key] = JSON.parse(value.to_json)
292-
end
293-
end
293+
@synced_attributes = {}
294+
@synced_attributes.each { |attribute, value| sync_attribute(key, value) }
294295
@changed_attributes = []
295296
@saving = false
296297
@errors = nil
@@ -300,8 +301,21 @@ def sync!(hash = {}) # does NOT notify (see saved! for notification)
300301
end
301302

302303
def sync_attribute(attribute, value)
304+
303305
@synced_attributes[attribute] = attributes[attribute] = value
304-
@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
306+
307+
#@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
308+
309+
if value.is_a? Collection
310+
@synced_attributes[attribute] = value.dup_for_sync
311+
elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
312+
value.backing_record.sync!
313+
elsif aggregation
314+
@synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
315+
elsif !model.reflect_on_association(attribute)
316+
@synced_attributes[attribute] = JSON.parse(value.to_json)
317+
end
318+
305319
@changed_attributes.delete(attribute)
306320
value
307321
end
@@ -381,7 +395,7 @@ def apply_method(method)
381395
else
382396
find_association(association, (id and id != "" and self.class.fetch_from_db([@model, [:find, id], method, @model.primary_key])))
383397
end
384-
elsif aggregation = @model.reflect_on_aggregation(method)
398+
elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
385399
new_from_vector(aggregation.klass, self, *vector, method)
386400
elsif id and id != ""
387401
self.class.fetch_from_db([@model, [:find, id], *method]) || self.class.load_from_db(self, *vector, method)
@@ -392,13 +406,16 @@ def apply_method(method)
392406
sync_attribute(method, new_value)
393407
elsif association = @model.reflect_on_association(method) and association.collection?
394408
@attributes[method] = Collection.new(association.klass, @ar_instance, association)
395-
elsif aggregation = @model.reflect_on_aggregation(method)
409+
elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
396410
@attributes[method] = aggregation.klass.new.tap do |aggregate|
397411
backing_record = aggregate.backing_record
398412
backing_record.aggregate_owner = self
399413
backing_record.aggregate_attribute = method
400414
end
401-
elsif method != model.primary_key
415+
elsif !aggregation and method != model.primary_key
416+
unless @attributes.has_key?(method)
417+
log("Warning: reading from new #{model.name}.#{method} before assignment. Will fetch value from server. This may not be what you expected!!", :warning)
418+
end
402419
new_value = self.class.load_from_db(self, *vector, method)
403420
new_value = @attributes[method] if new_value.is_a? DummyValue and @attributes.has_key?(method)
404421
sync_attribute(method, new_value)

lib/reactive_record/active_record/reactive_record/isomorphic_base.rb

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,11 @@ def self.gather_records(records_to_process, force, record_being_saved)
262262
else
263263
output_attributes[attribute] = nil
264264
end
265-
elsif record.model.reflect_on_aggregation(attribute)
265+
elsif aggregation = record.model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
266266
add_new_association.call record, attribute, value.backing_record
267+
elsif aggregation
268+
new_value = aggregation.serialize(value)
269+
output_attributes[attribute] = new_value if record.changed?(attribute) or new_value != aggregation.serialize(record.synced_attributes[attribute])
267270
elsif record.changed?(attribute)
268271
output_attributes[attribute] = value
269272
end
@@ -368,33 +371,41 @@ def self.save_records(models, associations, acting_user, validate, save)
368371
models.each do |model_to_save|
369372
attributes = model_to_save[:attributes]
370373
model = Object.const_get(model_to_save[:model])
371-
id = attributes.delete(model.primary_key) # if we are saving existing model primary key value will be present
374+
id = attributes.delete(model.primary_key) if model.respond_to? :primary_key # if we are saving existing model primary key value will be present
372375
vector = model_to_save[:vector]
373376
vector[0] = vector[0].constantize
374-
record = find_record(model, id, vector, save)
375-
reactive_records[model_to_save[:id]] = vectors[vector] = record and if record.id
377+
reactive_records[model_to_save[:id]] = vectors[vector] = record = find_record(model, id, vector, save)
378+
if record and record.respond_to?(:id) and record.id
379+
# we have an already exising activerecord model
376380
keys = record.attributes.keys
377381
attributes.each do |key, value|
378382
if keys.include? key
379383
record[key] = value
384+
elsif !value.nil? and aggregation = record.class.reflect_on_aggregation(key.to_sym) and !(aggregation.klass < ActiveRecord::Base)
385+
aggregation.mapping.each_with_index do |pair, i|
386+
record[pair.first] = value[i]
387+
end
380388
elsif record.respond_to? "#{key}="
381389
record.send("#{key}=",value)
382390
else
383391
# TODO once reading schema.rb on client is implemented throw an error here
384392
end
385393
end
386-
record
387-
else
394+
elsif record
395+
# either the model is new, or its not even an active record model
388396
keys = record.attributes.keys
389397
attributes.each do |key, value|
390398
if keys.include? key
391399
record[key] = value
392-
else
400+
elsif !value.nil? and aggregation = record.class.reflect_on_aggregation(key) and !(aggregation.klass < ActiveRecord::Base)
401+
aggregation.mapping.each_with_index do |pair, i|
402+
record[pair.first] = value[i]
403+
end
404+
elsif key.to_s != "id"
393405
record.send("#{key}=",value)
394406
end
395407
end
396408
new_models << record
397-
record
398409
end
399410
end
400411

@@ -472,7 +483,6 @@ def self.save_records(models, associations, acting_user, validate, save)
472483
rescue Exception => e
473484
puts "exception #{e}"
474485
puts e.backtrace.join("\n")
475-
binding.pry
476486
if save
477487
{success: false, saved_models: saved_models, message: e.message}
478488
else

lib/reactive_record/server_data_cache.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ def apply_method_to_cache(method, absolute_method, &block)
182182
self
183183
end
184184
rescue Exception => e
185-
binding.pry if cache_item.value and cache_item.value != []
186185
raise "ReactiveRecord exception caught when applying #{method} to db objects #{e}" if cache_item.value and cache_item.value != []
187186
representative
188187
end
@@ -219,7 +218,15 @@ def build_new_instances(method)
219218
end
220219
else
221220
apply_method_to_cache(method, method) do |cache_item|
222-
@preloaded_records[cache_item.absolute_vector + [method]] || cache_item.value.send(*method)
221+
if preloaded_value = @preloaded_records[cache_item.absolute_vector + [method]]
222+
preloaded_value
223+
elsif method.is_a? String and cache_item.value.class.respond_to?(:reflect_on_aggregation) and
224+
aggregation = cache_item.value.class.reflect_on_aggregation(method.to_sym) and !(aggregation.klass < ActiveRecord::Base) and
225+
cache_item.value.send(method)
226+
aggregation.mapping.collect { |attribute, accessor| cache_item.value[attribute] }
227+
else
228+
cache_item.value.send(*method)
229+
end
223230
end
224231
end
225232
end
@@ -297,6 +304,9 @@ def self.load_from_json(tree, target = nil)
297304
else
298305
target.backing_record.update_attribute([method], value.first)
299306
end
307+
elsif target.class.respond_to?(:reflect_on_aggregation) and aggregation = target.class.reflect_on_aggregation(method) and
308+
!(aggregation.klass < ActiveRecord::Base)
309+
target.send "#{method}=", aggregation.deserialize(value.first)
300310
elsif value.is_a? Array
301311
target.send "#{method}=", value.first unless method == "id" # we handle ids first so things sync nicely
302312
elsif value.is_a? Hash and value[:id] and value[:id].first #and

spec/test_app/Gemfile.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: ../..
33
specs:
4-
reactive-record (0.7.22)
4+
reactive-record (0.7.23)
55
opal-browser
66
opal-rails
77
rails (>= 3.2.13)
@@ -22,9 +22,9 @@ PATH
2222
PATH
2323
remote: ../../../reactive-ruby
2424
specs:
25-
reactive-ruby (0.7.22)
25+
reactive-ruby (0.7.23)
2626
opal
27-
opal-activesupport
27+
opal-activesupport (>= 0.2.0)
2828
opal-browser
2929

3030
GEM
@@ -101,7 +101,7 @@ GEM
101101
sourcemap (~> 0.1.0)
102102
sprockets (~> 3.1)
103103
tilt (>= 1.4)
104-
opal-activesupport (0.1.0)
104+
opal-activesupport (0.2.0)
105105
opal (>= 0.5.0, < 1.0.0)
106106
opal-browser (0.2.0)
107107
opal
@@ -165,7 +165,7 @@ GEM
165165
rspec-support (~> 3.3.0)
166166
rspec-support (3.3.0)
167167
sourcemap (0.1.1)
168-
sprockets (3.3.5)
168+
sprockets (3.4.0)
169169
rack (> 1, < 3)
170170
sprockets-rails (2.3.3)
171171
actionpack (>= 3.0)

spec/test_app/app/views/models.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
require_tree './models'
1+
require_tree './models'

spec/test_app/app/views/models/user.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
class TestData
2+
3+
def initialize(string, times)
4+
@string = string
5+
@times = times
6+
end
7+
8+
attr_accessor :string
9+
attr_accessor :times
10+
11+
def big_string
12+
puts "calling big_string #{string} * #{times}"
13+
string * times
14+
end
15+
16+
end
17+
18+
119
class User < ActiveRecord::Base
220

321
def view_permitted?(attribute)
@@ -12,6 +30,8 @@ def view_permitted?(attribute)
1230
composed_of :address, :class_name => 'Address', :constructor => :compose, :mapping => Address::MAPPED_FIELDS.map {|f| ["address_#{f}", f] }
1331
composed_of :address2, :class_name => 'Address', :constructor => :compose, :mapping => Address::MAPPED_FIELDS.map {|f| ["address2_#{f}", f] }
1432

33+
composed_of :data, :class_name => 'TestData', :allow_nil => true, :mapping => [['data_string', 'string'], ['data_times', 'times']]
34+
1535
def name
1636
"#{first_name} #{last_name}"
1737
end

spec/test_app/db/development.sqlite3

0 Bytes
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AddTestDataAttributesToUser < ActiveRecord::Migration
2+
def change
3+
add_column :users, :data_string, :string
4+
add_column :users, :data_times, :integer
5+
end
6+
end

0 commit comments

Comments
 (0)