Skip to content
Merged
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
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,31 +239,34 @@ end
##### Options

The association methods support the following options:

* `class_name` - a string specifying the underlying class for the related resource
* `foreign_key` - the method on the resource used to fetch the related resource. Defaults to `<resource_name>_id` for
has_one and `<resource_name>_ids` for has_many relationships.
* `foreign_key` - the method on the resource used to fetch the related resource. Defaults to `<resource_name>_id` for has_one and `<resource_name>_ids` for has_many relationships.
* `acts_as_set` - allows the entire set of related records to be replaced in one operation. Defaults to false if not set.
* `relation_name` - the name of the relation to use on the model. A lambda may be provided which allows conditional
selection of the relation based on the context.
* `relation_name` - the name of the relation to use on the model. A lambda may be provided which allows conditional selection of the relation based on the context.
* `polymorphic` - set to true to identify `has_one` associations that are polymorphic.

Examples:

```ruby
class CommentResource < JSONAPI::Resource
class CommentResource < JSONAPI::Resource
attributes :body
has_one :post
has_one :author, class_name: 'Person'
has_many :tags, acts_as_set: true
end
```
end

```ruby
class ExpenseEntryResource < JSONAPI::Resource
attributes :cost, :transaction_date

has_one :currency, class_name: 'Currency', foreign_key: 'currency_code'
has_one :employee
end

class TagResource < JSONAPI::Resource
attributes :name
has_one :taggable, polymorphic: true
end
```

```ruby
Expand All @@ -283,8 +286,14 @@ class BookResource < JSONAPI::Resource
}
...
end
```

The polymorphic association will require the resource and controller to exist, although routing to them will cause an error.

```ruby
class TaggableResource < JSONAPI::Resource; end
class TaggablesController < JSONAPI::ResourceController; end
```

#### Filters

Filters for locating objects of the resource type are specified in the resource definition. Single filters can be
Expand Down
31 changes: 24 additions & 7 deletions lib/jsonapi/association.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
module JSONAPI
class Association
attr_reader :acts_as_set, :foreign_key, :type, :options, :name, :class_name
attr_reader :acts_as_set, :foreign_key, :type, :options, :name,
:class_name, :polymorphic

def initialize(name, options = {})
@name = name.to_s
@options = options
@acts_as_set = options.fetch(:acts_as_set, false) == true
@foreign_key = options[:foreign_key] ? options[:foreign_key].to_sym : nil
@module_path = options[:module_path] || ''
@relation_name = options.fetch(:relation_name, @name)
@name = name.to_s
@options = options
@acts_as_set = options.fetch(:acts_as_set, false) == true
@foreign_key = options[:foreign_key] ? options[:foreign_key].to_sym : nil
@module_path = options[:module_path] || ''
@relation_name = options.fetch(:relation_name, @name)
@polymorphic = options.fetch(:polymorphic, false) == true
end

alias_method :polymorphic?, :polymorphic

def primary_key
@primary_key ||= resource_klass._primary_key
end
Expand All @@ -32,13 +36,26 @@ def relation_name(options = {})
end
end

def type_for_source(source)
if polymorphic?
resource = source.public_send(name)
resource.class._type if resource
else
type
end
end

class HasOne < Association
def initialize(name, options = {})
super
@class_name = options.fetch(:class_name, name.to_s.camelize)
@type = class_name.underscore.pluralize.to_sym
@foreign_key ||= "#{name}_id".to_sym
end

def polymorphic_type
"#{type.to_s.singularize}_type" if polymorphic?
end
end

class HasMany < Association
Expand Down
19 changes: 19 additions & 0 deletions lib/jsonapi/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,25 @@ def apply
end
end

class ReplacePolymorphicHasOneAssociationOperation < Operation
attr_reader :resource_id, :association_type, :key_value, :key_type

def initialize(resource_klass, options = {})
@resource_id = options.fetch(:resource_id)
@key_value = options.fetch(:key_value)
@key_type = options.fetch(:key_type)
@association_type = options.fetch(:association_type).to_sym
super(resource_klass, options)
end

def apply
resource = @resource_klass.find_by_key(@resource_id, context: @context)
result = resource.replace_polymorphic_has_one_link(@association_type, @key_value, @key_type)

return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
end
end

class CreateHasManyAssociationOperation < Operation
attr_reader :resource_id, :association_type, :data

Expand Down
1 change: 1 addition & 0 deletions lib/jsonapi/operations_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class OperationsProcessor
:remove_resource_operation,
:replace_fields_operation,
:replace_has_one_association_operation,
:replace_polymorphic_has_one_association_operation,
:create_has_many_association_operation,
:replace_has_many_association_operation,
:remove_has_many_association_operation,
Expand Down
56 changes: 33 additions & 23 deletions lib/jsonapi/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def check_include(resource_klass, include_parts)
association = resource_klass._association(association_name)
if association && format_key(association_name) == include_parts.first
unless include_parts.last.empty?
check_include(Resource.resource_for(@resource_klass.module_path + association.class_name.to_s), include_parts.last.partition('.'))
check_include(Resource.resource_for(@resource_klass.module_path + association.class_name.to_s.underscore), include_parts.last.partition('.'))
end
else
@errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
Expand Down Expand Up @@ -403,11 +403,8 @@ def parse_params(params, allowed_fields)
end

links_object = parse_has_one_links_object(linkage)
# Since we do not yet support polymorphic associations we will raise an error if the type does not match the
# association's type.
# TODO: Support Polymorphic associations
if links_object[:type] && (links_object[:type].to_s != association.type.to_s)
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
if !association.polymorphic? && links_object[:type] && (links_object[:type].to_s != association.type.to_s)
raise JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
end

unless links_object[:id].nil?
Expand Down Expand Up @@ -520,18 +517,31 @@ def parse_add_association_operation(data, association_type, parent_key)

def parse_update_association_operation(data, association_type, parent_key)
association = resource_klass._association(association_type)

if association.is_a?(JSONAPI::Association::HasOne)
object_params = { relationships: { format_key(association.name) => { data: data } } }
verified_param_set = parse_params(object_params, updatable_fields)

@operations.push JSONAPI::ReplaceHasOneAssociationOperation.new(
resource_klass,
context: @context,
resource_id: parent_key,
association_type: association_type,
key_value: verified_param_set[:has_one].values[0]
)
if association.polymorphic?
object_params = {relationships: {format_key(association.name) => {data: data}}}
verified_param_set = parse_params(object_params, updatable_fields)

@operations.push JSONAPI::ReplacePolymorphicHasOneAssociationOperation.new(
resource_klass,
context: @context,
resource_id: parent_key,
association_type: association_type,
key_value: verified_param_set[:has_one].values[0],
key_type: data['type']
)
else
object_params = {relationships: {format_key(association.name) => {data: data}}}
verified_param_set = parse_params(object_params, updatable_fields)

@operations.push JSONAPI::ReplaceHasOneAssociationOperation.new(
resource_klass,
context: @context,
resource_id: parent_key,
association_type: association_type,
key_value: verified_param_set[:has_one].values[0]
)
end
else
unless association.acts_as_set
fail JSONAPI::Exceptions::HasManySetReplacementForbidden.new
Expand All @@ -541,12 +551,12 @@ def parse_update_association_operation(data, association_type, parent_key)
verified_param_set = parse_params(object_params, updatable_fields)

@operations.push JSONAPI::ReplaceHasManyAssociationOperation.new(
resource_klass,
context: @context,
resource_id: parent_key,
association_type: association_type,
data: verified_param_set[:has_many].values[0]
)
resource_klass,
context: @context,
resource_id: parent_key,
association_type: association_type,
data: verified_param_set[:has_many].values[0]
)
end
end

Expand Down
37 changes: 30 additions & 7 deletions lib/jsonapi/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Resource
:replace_has_many_links,
:create_has_one_link,
:replace_has_one_link,
:replace_polymorphic_has_one_link,
:remove_has_many_link,
:remove_has_one_link,
:replace_fields
Expand Down Expand Up @@ -79,6 +80,12 @@ def replace_has_one_link(association_type, association_key_value)
end
end

def replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
change :replace_polymorphic_has_one_link do
_replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
end
end

def remove_has_many_link(association_type, key)
change :remove_has_many_link do
_remove_has_many_link(association_type, key)
Expand Down Expand Up @@ -188,6 +195,17 @@ def _replace_has_one_link(association_type, association_key_value)
:completed
end

def _replace_polymorphic_has_one_link(association_type, key_value, key_type)
association = self.class._associations[association_type]

send("#{association.foreign_key}=", key_value)
send("#{association.polymorphic_type}=", key_type.singularize.capitalize)

@save_needed = true

:completed
end

def _remove_has_many_link(association_type, key)
association = self.class._associations[association_type]

Expand Down Expand Up @@ -632,12 +650,11 @@ def _associate(klass, *attrs)

attrs.each do |attr|
check_reserved_association_name(attr)

association = @_associations[attr] = klass.new(attr, options)
@_associations[attr] = association = klass.new(attr, options)

associated_records_method_name = case association
when JSONAPI::Association::HasOne then "record_for_#{attr}"
when JSONAPI::Association::HasMany then "records_for_#{attr}"
when JSONAPI::Association::HasOne then "record_for_#{attr}"
when JSONAPI::Association::HasMany then "records_for_#{attr}"
end

foreign_key = association.foreign_key
Expand All @@ -657,10 +674,16 @@ def _associate(klass, *attrs)
end unless method_defined?(foreign_key)

define_method attr do |options = {}|
resource_klass = association.resource_klass
if resource_klass
if association.polymorphic?
associated_model = public_send(associated_records_method_name)
return associated_model ? resource_klass.new(associated_model, @context) : nil
resource_klass = Resource.resource_for(self.class.module_path + associated_model.class.to_s.underscore) if associated_model
return resource_klass.new(associated_model, @context) if resource_klass
else
resource_klass = association.resource_klass
if resource_klass
associated_model = public_send(associated_records_method_name)
return associated_model ? resource_klass.new(associated_model, @context) : nil
end
end
end unless method_defined?(attr)
elsif association.is_a?(JSONAPI::Association::HasMany)
Expand Down
3 changes: 2 additions & 1 deletion lib/jsonapi/resource_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def relationship_data(source, include_directives)
resource = source.send(name)
if resource
id = resource.id
type = association.type_for_source(source)
associations_only = already_serialized?(type, id)
if include_linkage && !associations_only
add_included_object(type, id, object_hash(resource, ia))
Expand Down Expand Up @@ -232,7 +233,7 @@ def has_one_linkage(source, association)
linkage = {}
linkage_id = foreign_key_value(source, association)
if linkage_id
linkage[:type] = format_key(association.type)
linkage[:type] = format_key(association.type_for_source(source))
linkage[:id] = linkage_id
else
linkage = nil
Expand Down
9 changes: 7 additions & 2 deletions lib/jsonapi/routing_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,13 @@ def jsonapi_related_resource(*association)
association = source._associations[association_name]

formatted_association_name = format_route(association.name)
related_resource = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(association.class_name.underscore.pluralize))
options[:controller] ||= related_resource._type.to_s

if association.polymorphic?
options[:controller] ||= association.class_name.underscore.pluralize
else
related_resource = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(association.class_name.underscore.pluralize))
options[:controller] ||= related_resource._type.to_s
end

match "#{formatted_association_name}", controller: options[:controller],
association: association.name, source: resource_type_with_module_prefix(source._type),
Expand Down
Loading