Skip to content

Commit

Permalink
Added support for Reference/PolymorphicReference/ReferenceCollection/…
Browse files Browse the repository at this point in the history
…PolymorphicReferenceCollection plus several bugfixes
  • Loading branch information
vihai committed Oct 17, 2013
1 parent bde244a commit 13e2a09
Show file tree
Hide file tree
Showing 8 changed files with 724 additions and 458 deletions.
124 changes: 84 additions & 40 deletions lib/active_rest/model/interface.rb
Expand Up @@ -581,14 +581,38 @@ def apply_model_attributes(obj, values, opts = {})
raise AttributeNotWritable.new(obj, attr_name) if !writable

case attr
when Attribute::Reference
when Attribute::EmbeddedModel
record = obj.send(attr_name)
when Attribute::Reference, Attribute::PolymorphicReference
value = value.with_indifferent_access if value
association = obj.association(attr_name)

if association.loaded?
record = association.target
else
record = association.reload.target
end

if value && attr.is_a?(Attribute::PolymorphicReference)
association.target = value[:_type].constantize.find(value[:id])
elsif value
association.target = association.klass.find(value[:id])
else
association.target = nil
end

when Attribute::EmbeddedModel, Attribute::EmbeddedPolymorphicModel
value = value.with_indifferent_access if value
association = obj.association(attr_name)

if !value || value['_destroy']
if association.loaded?
record = association.target
else
record = association.reload.target
end

if !value || value[:_destroy]
# DESTROY
record.mark_for_destruction if record
elsif record && value['id'] && value['id'] != 0 && record.id == value['id']
elsif record
# UPDATE
record.ar_apply_update_attributes(@name, value, opts)
else
Expand All @@ -598,68 +622,84 @@ def apply_model_attributes(obj, values, opts = {})
record.mark_for_destruction if record

newrecord = nil
if @allow_polymorphic_creation && value.has_key('_type') && value['_type']
newrecord = value['_type'].constantize.new
if attr.is_a?(Attribute::EmbeddedPolymorphicModel)
raise TypeMissing if !value[:_type]
# XXX TODO Fail gracefully if type is not found
newrecord = value[:_type].constantize.ar_new(@name, value, opts)
else
newrecord = obj.send("build_#{attr_name}")
newrecord = association.klass.ar_new(@name, value, opts)
end

newrecord.ar_apply_creation_attributes(@name, value, opts)
association.target = newrecord
end

when Attribute::UniformModelsCollection

association = obj.association(attr_name)

existing_records = if association.loaded?
association.target
if association.loaded?
existing_records = association.target
else
ids = value.map {|a| a['id'] || a[:id] }.compact
ids.empty? ? [] : association.scope.where(association.klass.primary_key => ids)
existing_records = ids.empty? ? [] : association.scope.where(association.klass.primary_key => ids)
end

value.each do |attributes|
attributes = attributes.with_indifferent_access
value.each do |val|
val = val.with_indifferent_access

if attributes['id'].blank? || attributes['id'] == 0
# XXX Evaluate if id==9 is to be considered an indication to create record
if !val.has_key?(:id) || val[:id].blank? || val[:id] == 0
# CREATE

if attributes['_type'] && attr.model_class.constantize.interfaces[@name].allow_polymorphic_creation
newrecord = attributes[:_type].constantize.ar_new(@name, attributes, opts)
association.concat(newrecord)
if attr.model_class.constantize.interfaces[@name].allow_polymorphic_creation
raise TypeMising if !val[:_type]
# XXX TODO Fail gracefully if type is not found
newrecord = val[:_type].constantize.ar_new(@name, val, opts)
else
newrecord = association.build
newrecord.ar_apply_creation_attributes(@name, attributes, opts)
raise ClassDoesNotMatch.new(obj.class, association.klass) if val[:_type] && val[:_type] != association.klass.name
newrecord = association.klass.ar_new(@name, val, opts)
end

elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }

unless association.loaded?
target_record = association.target.detect { |record| record == existing_record }

if target_record
existing_record = target_record
else
association.add_to_target(existing_record)
end
end
association.concat(newrecord)
else
existing_record = existing_records.detect { |x| x.id == val[:id] }
raise AssociatedRecordNotFound.new if !existing_record

if attributes['_destroy']
if val[:_destroy]
# DESTROY
existing_record.destroy
existing_record.mark_for_destruction
else
# UPDATE
existing_record.ar_apply_update_attributes(@name, attributes, opts)
existing_record.ar_apply_update_attributes(@name, val, opts)
end
else
raise AssociatedRecordNotFound.new
end
end

when Attribute::UniformReferencesCollection
when Attribute::EmbeddedPolymorphicModel
when Attribute::PolymorphicReference
association = obj.association(attr_name)

if association.loaded?
existing_records = association.target
else
ids = value.map {|a| a['id'] || a[:id] }.compact
existing_records = ids.empty? ? [] : association.scope.where(association.klass.primary_key => ids)
end

value.each do |val|
val = val.with_indifferent_access

existing_record = existing_records.detect { |x| x.id == val[:id] }

if val[:_destroy]
raise AssociatedRecordNotFound.new if !existing_record
existing_record.destroy
elsif !existing_record
association.concat(association.klass.find(val[:id]))
end
end

when Attribute::PolymorphicModelsCollection
# Not supported because ActiveRecord has no concept of polymorphoc has_many
when Attribute::PolymorphicReferencesCollection
# Not supported because ActiveRecord has no concept of polymorphoc has_many

when Attribute::Structure, Attribute
obj.send("#{attr_name}=", value)
Expand Down Expand Up @@ -754,6 +794,8 @@ class ClassDoesNotMatch < Error
attr_accessor :type

def initialize(model_class, type)
super

@model_class = model_class
@model_type = type
end
Expand All @@ -763,6 +805,8 @@ def to_s
end
end

class TypeMissing < Error
end
end

end
Expand Down
2 changes: 1 addition & 1 deletion lib/active_rest/version.rb
@@ -1,3 +1,3 @@
module ActiveRest
VERSION = '6.1.2'
VERSION = '6.2.0'
end
12 changes: 6 additions & 6 deletions specapp/Gemfile.lock
Expand Up @@ -14,7 +14,7 @@ GIT
PATH
remote: ..
specs:
active_rest (6.1.1)
active_rest (6.2.0)
squeel

GEM
Expand Down Expand Up @@ -63,7 +63,7 @@ GEM
mime-types (1.25)
mini_portile (0.5.1)
minitest (4.7.5)
multi_json (1.8.1)
multi_json (1.8.2)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
polyamorous (0.6.4)
Expand All @@ -90,10 +90,10 @@ GEM
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
rspec-core (2.14.5)
rspec-core (2.14.6)
rspec-expectations (2.14.3)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.14.3)
rspec-mocks (2.14.4)
rspec-rails (2.14.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
Expand All @@ -106,7 +106,7 @@ GEM
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.0.0)
sprockets-rails (2.0.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (~> 2.8)
Expand All @@ -124,7 +124,7 @@ GEM
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.37)
tzinfo (0.3.38)

PLATFORMS
ruby
Expand Down
10 changes: 6 additions & 4 deletions specapp/app/models/company.rb
Expand Up @@ -55,11 +55,13 @@ class Bar < ActiveRecord::Base

has_many :phones,
:class_name => 'Company::Phone',
:embedded => true
:embedded => true,
:autosave => true

belongs_to :location,
:class_name => 'CompanyLocation',
:embedded => true
:embedded => true,
:autosave => true

composed_of :full_address,
:class_name => '::Company::FullAddress',
Expand All @@ -69,8 +71,8 @@ class Bar < ActiveRecord::Base
[ :zip, :zip ],
]

belongs_to :object_1, :polymorphic => true, :embedded => true
belongs_to :object_2, :polymorphic => true, :embedded => true
belongs_to :object_1, :polymorphic => true, :embedded => true, :autosave => true
belongs_to :object_2, :polymorphic => true, :embedded => true, :autosave => true

belongs_to :polyref_1, :polymorphic => true
belongs_to :polyref_2, :polymorphic => true
Expand Down
35 changes: 31 additions & 4 deletions specapp/spec/factories/companies.rb
Expand Up @@ -45,25 +45,52 @@
end

factory :group, :class => 'Group' do
id 1001
name 'MegaHolding'
end

factory :group_2, :class => 'Group' do
id 1002
name 'CompuGlobalHyperMegaNet'
end

factory :company_location, :class => 'CompanyLocation' do
lat 0.12345
lon 9.12345
raw_name 'Seveso'
association :coordinate, :factory => :company_location_coordinate
end

factory :company_location2, :class => 'CompanyLocation' do
lat 50.12345
lon 59.12345
raw_name 'Abaca'
association :coordinate, :factory => :company_location_coordinate2
end

factory :company_location3, :class => 'CompanyLocation' do
lat 90.12345
lon 99.12345
raw_name 'Zulu'
association :coordinate, :factory => :company_location_coordinate3
end

factory :company_location_coordinate, :class => 'CompanyLocationCoordinate' do
lat 0.12345
lon 9.12345
order 1
end

factory :company_location_coordinate2, :class => 'CompanyLocationCoordinate' do
lat 50.12345
lon 59.12345
order 2
end

factory :company_location_coordinate3, :class => 'CompanyLocationCoordinate' do
lat 90.12345
lon 99.12345
order 3
end

factory :company_phone, :class => 'Company::Phone' do
Expand Down Expand Up @@ -96,12 +123,12 @@
not_readable_attribute 232323
not_writable_attribute 343434

users { [association(:user, :name => 'Paolino Paperino'),
association(:user, :name => 'Zio Paperone')] }
users { [association(:user, :id => 1001, :name => 'Paolino Paperino'),
association(:user, :id => 1002, :name => 'Zio Paperone')] }

association :location, :factory => :company_location
phones { [association(:company_phone, :number => 99999999),
association(:company_phone, :number => 12345678)] }
phones { [association(:company_phone, :id => 1001, :number => 99999999),
association(:company_phone, :id => 1002, :number => 12345678)] }

association :group, :factory => :group

Expand Down
24 changes: 12 additions & 12 deletions specapp/spec/models/account_spec.rb
Expand Up @@ -253,9 +253,9 @@ def initialize(global_capabilities, auth_identity)

describe 'ar_serializable_hash' do
before(:each) do
@a1= FactoryGirl.create(:account1)
@a2= FactoryGirl.create(:account2)
@a3= FactoryGirl.create(:account3)
@a1 = FactoryGirl.create(:account1)
@a2 = FactoryGirl.create(:account2)
@a3 = FactoryGirl.create(:account3)
end

it 'raises ResourceNotReadable for Account1 in context_1' do
Expand Down Expand Up @@ -354,9 +354,9 @@ def initialize(global_capabilities, auth_identity)

describe 'apply_update_attributes' do
before(:each) do
@a1= FactoryGirl.create(:account1)
@a2= FactoryGirl.create(:account2)
@a3= FactoryGirl.create(:account3)
@a1 = FactoryGirl.create(:account1)
@a2 = FactoryGirl.create(:account2)
@a3 = FactoryGirl.create(:account3)
end

it 'does not raise an error when empty update is specified' do
Expand Down Expand Up @@ -399,9 +399,9 @@ def initialize(global_capabilities, auth_identity)

describe 'ar_apply_update_attributes' do
before(:each) do
@a1= FactoryGirl.create(:account1)
@a2= FactoryGirl.create(:account2)
@a3= FactoryGirl.create(:account3)
@a1 = FactoryGirl.create(:account1)
@a2 = FactoryGirl.create(:account2)
@a3 = FactoryGirl.create(:account3)
end

it 'does not raise an error when empty update is specified if the resource is accessible in some way' do
Expand Down Expand Up @@ -465,9 +465,9 @@ def initialize(global_capabilities, auth_identity)

describe 'interface[:rest].allow_action?' do
before(:each) do
@a1= FactoryGirl.create(:account1)
@a2= FactoryGirl.create(:account2)
@a3= FactoryGirl.create(:account3)
@a1 = FactoryGirl.create(:account1)
@a2 = FactoryGirl.create(:account2)
@a3 = FactoryGirl.create(:account3)
end

it 'allows special_action to users with special_functions capabilities' do
Expand Down

0 comments on commit 13e2a09

Please sign in to comment.