Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use nested path parameters from the parent when using build #318

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ Bundler/DuplicatedGem:
Exclude:
- 'Gemfile'

# Offense count: 1
Lint/EmptyWhen:
Exclude:
- 'lib/her/model/parse.rb'

# Offense count: 1
Lint/IneffectiveAccessModifier:
Exclude:
Expand Down
18 changes: 17 additions & 1 deletion lib/her/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ def request(opts = {})
method = opts.delete(:_method)
path = opts.delete(:_path)
headers = opts.delete(:_headers)
opts.delete_if { |key, _| key.to_s =~ /^_/ } # Remove all internal parameters

# Recursively remove all internal parameters
strip_internal_params opts

if method == :options
# Faraday doesn't support the OPTIONS verb because of a name collision with an internal options method
# so we need to call run_request directly.
Expand Down Expand Up @@ -117,5 +120,18 @@ def request(opts = {})
def self.default_api(opts = {})
defined?(@default_api) ? @default_api : nil
end

# @private
def strip_internal_params(object)
case object
when Hash
object.delete_if { |key, _| key.to_s[0] == '_' }
strip_internal_params object.values
when Array
object.each do |elem|
strip_internal_params elem
end
end
end
end
end
6 changes: 3 additions & 3 deletions lib/her/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ module Her
module Errors
class PathError < StandardError

attr_reader :missing_parameter
attr_reader :missing_parameters

def initialize(message, missing_parameter = nil)
def initialize(message, missing_parameters = nil)
super(message)
@missing_parameter = missing_parameter
@missing_parameters = missing_parameters
end
end

Expand Down
4 changes: 3 additions & 1 deletion lib/her/model.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "active_model"
require "her/model/base"
require "her/model/deprecated_methods"
require "her/model/http"
Expand All @@ -7,9 +8,9 @@
require "her/model/parse"
require "her/model/associations"
require "her/model/introspection"
require "her/model/serialization"
require "her/model/paths"
require "her/model/nested_attributes"
require "active_model"

module Her
# This module is the main element of Her. After creating a Her::API object,
Expand All @@ -33,6 +34,7 @@ module Model
include Her::Model::HTTP
include Her::Model::Parse
include Her::Model::Introspection
include Her::Model::Serialization
include Her::Model::Paths
include Her::Model::Associations
include Her::Model::NestedAttributes
Expand Down
116 changes: 106 additions & 10 deletions lib/her/model/associations/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Association
# @private
def initialize(parent, opts = {})
@parent = parent
@parent_name = @parent.singularized_resource_name.to_sym
@opts = opts
@params = {}

Expand Down Expand Up @@ -40,34 +41,127 @@ def assign_single_nested_attributes(attributes)
end

# @private
def fetch(opts = {})
attribute_value = @parent.attributes[@name]
return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && (attribute_value.nil? || !attribute_value.nil? && attribute_value.empty?) && @params.empty?
def inverse
if @opts[:inverse_of]
iname = @opts[:inverse_of].to_sym
inverse = @klass.associations[:belongs_to].find { |assoc| assoc[:name].to_sym == iname }
raise Her::Errors::AssociationUnknownError, "Unknown association name :#{iname}" unless inverse
else
inverse = @klass.associations[:belongs_to].find { |assoc| assoc[:name].to_sym == @parent_name }
end

inverse
end

# @private
def set_missing_from_parent(missing, attributes, inverse = self.inverse)
missing.each do |m|
if inverse && inverse[:foreign_key].to_sym == m
attributes[m] = @parent.id
elsif m == :"#{@parent_name}_id"
attributes[:"_#{m}"] = @parent.id
else
id = @parent.get_attribute(m) || @parent.get_attribute(:"_#{m}")
attributes[:"_#{m}"] = id if id
end
end
end

return @cached_result unless @params.any? || @cached_result.nil?
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
return @opts[:default].try(:dup) if @parent.new?
# @private
def set_missing_and_inverse_from_parent(resources, inverse = self.inverse)
Array(resources).each do |resource|
begin
resource.request_path
rescue Her::Errors::PathError => e
set_missing_from_parent e.missing_parameters, resource.attributes, inverse
end

path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}" }
@klass.get(path, @params).tap do |result|
@cached_result = result unless @params.any?
iname = inverse ? inverse[:name] : @parent_name
resource.send("#{iname}=", @parent)
end
end

# @private
def fetch(opts = {})
if @params.blank?
result =
if @parent.attributes.key?(@name) && @parent.attributes[@name].blank?
@opts[:default].try(:dup)
else
@cached_result || @parent.attributes[@name]
end
end

result ||=
if @parent.new?
@opts[:default].try(:dup)
else
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}" }
@klass.get(path, @params).tap { |r| @cached_result = r if @params.blank? }
end

set_missing_and_inverse_from_parent result if result
result
end

# @private
def build_association_path(code)
instance_exec(&code)
rescue Her::Errors::PathError
nil
end

# @private
def build_with_inverse(attributes = {})
inverse = self.inverse

attributes =
if inverse
attributes.merge(inverse[:name] => @parent)
else
attributes.merge(:"#{@parent_name}_id" => @parent.id)
end

retried = false

begin
resource = @klass.build(attributes)
resource.request_path
resource
rescue Her::Errors::PathError => e
if resource
attributes = resource.attributes
elsif retried
raise
end

set_missing_from_parent e.missing_parameters, attributes, inverse

if resource
resource
else
retried = true
retry
end
end
end

# @private
def reset
@params = {}
@cached_result = nil
@parent.attributes.delete(@name)
end

# @private
def assign(resources)
reset
set_missing_and_inverse_from_parent(resources)
@parent.attributes[@name] = resources
resources = @parent.attributes[@name] if ActiveSupport::VERSION::MAJOR == 3
@cached_result = resources
end

# Add query parameters to the HTTP request performed to fetch the data
#
# @example
Expand Down Expand Up @@ -97,7 +191,9 @@ def where(params = {})
def find(id)
return nil if id.blank?
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}/#{id}" }
@klass.get_resource(path, @params)
@klass.get_resource(path, @params).tap do |result|
set_missing_and_inverse_from_parent(result) if result
end
end

# Refetches the association and puts the proxy back in its initial state,
Expand Down
29 changes: 28 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 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::BelongsToAssociation.proxy(self, #{opts.inspect}))
end

def #{name}=(resource)
send("#{name}").association.assign(resource)
end
RUBY
end

Expand Down Expand Up @@ -86,6 +91,28 @@ def fetch
end
end

# @private
def assign(resource)
reset
pkey = resource ? resource.id : nil
@parent.send("#{@opts[:foreign_key]}=", pkey)
@parent.attributes[@name] = resource
@cached_result = resource

if resource
begin
@parent.request_path
rescue Her::Errors::PathError => e
e.missing_parameters.each do |m|
id = resource.get_attribute(m) || resource.get_attribute("_#{m}")
@parent.send("_#{m}=", id) if id
end
end
end

resource
end

# @private
def assign_nested_attributes(attributes)
assign_single_nested_attributes(attributes)
Expand Down
24 changes: 10 additions & 14 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 @@ -49,10 +54,8 @@ def self.parse(association, klass, data)
# user = User.find(1)
# new_comment = user.comments.build(:body => "Hello!")
# new_comment # => #<Comment user_id=1 body="Hello!">
# TODO: This only merges the id of the parents, handle the case
# where this is more deeply nested
def build(attributes = {})
@klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
build_with_inverse(attributes)
end

# Create a new object, save it and add it to the associated collection
Expand All @@ -72,23 +75,16 @@ 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
end

# @private
def fetch
super.tap do |o|
inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
o.each { |entry| entry.attributes[inverse_of] = @parent }
end
end

# @private
def assign_nested_attributes(attributes)
data = attributes.is_a?(Hash) ? attributes.values : attributes
Expand Down
12 changes: 9 additions & 3 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 All @@ -45,7 +50,7 @@ def self.parse(*args)
# new_role = user.role.build(:title => "moderator")
# new_role # => #<Role user_id=1 title="moderator">
def build(attributes = {})
@klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
build_with_inverse(attributes)
end

# Create a new object, save it and associate it to the parent
Expand All @@ -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
Loading