Skip to content

Commit

Permalink
Control whether associations are nested in parent saves using autosave
Browse files Browse the repository at this point in the history
This new association option is similar to the option of the same name
found in ActiveRecord. When true, associated resources are always
nested in a parent save. When false, they are never nested. When nil,
only new resources are nested. Unlike ActiveRecord, the default is
true (rather than nil) in order to preserve existing behaviour.

This is also subject to whether send_only_modified_attributes is true
or not. When true, only modified resources will be sent, even if
autosave is true. Unlike before, it won't do strange things like fetch
an existing has_one resource when you assign a new one because of
change tracking.

4 of the specs currently fail because they require the fixes in my
fix-change-tracking branch.
  • Loading branch information
chewi committed Dec 20, 2017
1 parent 9c4db32 commit 42cb5b9
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 27 deletions.
8 changes: 8 additions & 0 deletions lib/her/model/associations/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ def reset
@parent.attributes.delete(@name)
end

# @private
def assign(resources)
reset
set_missing_and_inverse_from_parent(resources)
@parent.attributes[@name] = resources
@cached_result = resources
end

# Add query parameters to the HTTP request performed to fetch the data
#
# @example
Expand Down
4 changes: 3 additions & 1 deletion lib/her/model/associations/belongs_to_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def self.attach(klass, name, opts)
:data_key => name,
:default => nil,
:foreign_key => "#{name}_id",
:path => "/#{name.to_s.pluralize}/:id"
:path => "/#{name.to_s.pluralize}/:id",
:autosave => true
}.merge(opts)
klass.associations[:belongs_to] << opts

Expand Down Expand Up @@ -95,6 +96,7 @@ def assign(resource)
reset
pkey = resource ? resource.id : nil
@parent.send("#{@opts[:foreign_key]}=", pkey)
@parent.attributes[@name] = resource
@cached_result = resource

if resource
Expand Down
12 changes: 9 additions & 3 deletions lib/her/model/associations/has_many_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def self.attach(klass, name, opts)
:data_key => name,
:default => Her::Collection.new,
:path => "/#{name}",
:inverse_of => nil
:inverse_of => nil,
:autosave => true
}.merge(opts)
klass.associations[:has_many] << opts

Expand All @@ -22,6 +23,10 @@ def #{name}
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.proxy(self, #{opts.inspect}))
end
def #{name}=(resources)
send("#{name}").association.assign(resources)
end
RUBY
end

Expand Down Expand Up @@ -70,10 +75,11 @@ def build(attributes = {})
# user.comments # => [#<Comment id=2 user_id=1 body="Hello!">]
def create(attributes = {})
resource = build(attributes)
set_missing_and_inverse_from_parent(resource)

if resource.save
@parent.attributes[@name] ||= Her::Collection.new
@parent.attributes[@name] << resource
collection = @parent.attributes[@name] || assign(Her::Collection.new)
collection << resource
end

resource
Expand Down
10 changes: 8 additions & 2 deletions lib/her/model/associations/has_one_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def self.attach(klass, name, opts)
:name => name,
:data_key => name,
:default => nil,
:path => "/#{name}"
:path => "/#{name}",
:autosave => true
}.merge(opts)
klass.associations[:has_one] << opts

Expand All @@ -21,6 +22,10 @@ def #{name}
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.proxy(self, #{opts.inspect}))
end
def #{name}=(resource)
send("#{name}").association.assign(resource)
end
RUBY
end

Expand Down Expand Up @@ -65,7 +70,8 @@ def build(attributes = {})
# user.role # => #<Role id=2 user_id=1 title="moderator">
def create(attributes = {})
resource = build(attributes)
@parent.attributes[@name] = resource if resource.save
set_missing_and_inverse_from_parent(resource)
assign(resource) if resource.save
resource
end

Expand Down
2 changes: 1 addition & 1 deletion lib/her/model/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def use_setter_methods(model, params = {})
setter_method_names = model.class.setter_method_names
params.each_with_object({}) do |(key, value), memo|
setter_method = key.to_s + '='
if setter_method_names.include?(setter_method) && (!assoc_keys.include?(key) || !value.is_a?(Hash))
if setter_method_names.include?(setter_method) && (!assoc_keys.include?(key) || (!value.is_a?(Hash) && Array(value).any? { |v| !v.is_a?(Hash) }))
model.send(setter_method, value)
else
memo[key.to_sym] = value
Expand Down
48 changes: 38 additions & 10 deletions lib/her/model/parse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ def parse(data)
# @private
def to_params(attributes, changes={})
filtered_attributes = attributes.dup.symbolize_keys
filtered_attributes.merge!(embeded_params(attributes))

if her_api.options[:send_only_modified_attributes]
filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
hash[attribute] = filtered_attributes[attribute]
hash
end
end

embed_params!(attributes, filtered_attributes)

if include_root_in_json?
if json_api_format?
{ included_root_element => [filtered_attributes] }
Expand All @@ -53,16 +55,42 @@ def to_params(attributes, changes={})
end

# @private
def embeded_params(attributes)
associations.values.flatten.each_with_object({}) do |definition, hash|
value = case association = attributes[definition[:name]]
when Her::Collection, Array
association.map { |a| a.to_params }.reject(&:empty?)
when Her::Model
association.to_params
end
hash[definition[:data_key]] = value if value.present?
def embed_params!(read_attributes, write_attributes)
first = Thread.current[:her_embedded_params_objects].nil?
Thread.current[:her_embedded_params_objects] = [] if first

return {} if Thread.current[:her_embedded_params_objects].include?(self)
Thread.current[:her_embedded_params_objects] << self

associations.values.flatten.each do |assoc|
write_attributes.delete(assoc[:name])
next if assoc[:autosave] == false
value = read_attributes[assoc[:name]]

value =
if assoc[:autosave].nil?
case value
when Her::Collection, Array
value.select(&:new_record?)
when Her::Model
value if value.new_record?
end
else
value
end

value =
case value
when Her::Collection, Array
value.map(&:to_params).reject(&:empty?)
when Her::Model
value.to_params
end

write_attributes[assoc[:data_key]] = value if value.present?
end
ensure
Thread.current[:her_embedded_params_objects] = nil if first
end

# Return or change the value of `include_root_in_json`
Expand Down
Loading

0 comments on commit 42cb5b9

Please sign in to comment.