Sitelet https://github.com/jsonapi-serializer/jsonapi-serializer/pull/60/files
Skip to content
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
6 changes: 6 additions & 0 deletions lib/fast_jsonapi/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module FastJsonapi

class InvalidIncludeError < ArgumentError
end

end
42 changes: 24 additions & 18 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,7 @@ def process_options(options)
@params = options[:params] || {}
raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)

if options[:include].present?
@includes = options[:include].reject(&:blank?).map(&:to_sym)
self.class.validate_includes!(@includes)
end
@includes = parse_includes(options[:include])
end

def deep_symbolize(collection)
Expand All @@ -115,6 +112,29 @@ def is_collection?(resource, force_is_collection = nil)
resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
end

def parse_includes(includes, output = {})
return if !includes

if includes.is_a?(Hash)
includes.each_pair do |k, v|
key = k.to_sym
output[key] ||= {}
output[key].merge!(parse_includes(v))
end
elsif includes.is_a?(Array)
includes.each { |i| parse_includes(i, output) }
elsif includes.is_a?(String) || includes.is_a?(Symbol)
parent = output
segments = includes.to_s.split('.').map(&:to_sym)
segments.each do |s|
parent[s] ||= {}
parent = parent[s]
end
end

return output
end

class_methods do

def inherited(subclass)
Expand Down Expand Up @@ -304,20 +324,6 @@ def link(*params, &block)
options: options
)
end

def validate_includes!(includes)
return if includes.blank?

includes.each do |include_item|
klass = self
parse_include_item(include_item).each do |parsed_include|
relationships_to_serialize = klass.relationships_to_serialize || {}
relationship_to_include = relationships_to_serialize[parsed_include]
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
klass = relationship_to_include.serializer.to_s.constantize unless relationship_to_include.polymorphic.is_a?(Hash)
end
end
end
end
end
end
70 changes: 27 additions & 43 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'active_support/concern'
require 'fast_jsonapi/multi_to_json'
require 'fast_jsonapi/errors'

module FastJsonapi
MandatoryField = Class.new(StandardError)
Expand Down Expand Up @@ -57,7 +58,7 @@ def relationships_hash(record, relationships = nil, fieldset = nil, includes_lis
relationships = {} if fieldset == []

relationships.each_with_object({}) do |(key, relationship), hash|
included = includes_list.present? && includes_list.include?(key)
included = includes_list.present? && includes_list.has_key?(key)
relationship.serialize(record, included, params, hash)
end
end
Expand Down Expand Up @@ -101,58 +102,41 @@ def to_json(payload)
FastJsonapi::MultiToJson.to_json(payload) if payload.present?
end

def parse_include_item(include_item)
return [include_item.to_sym] unless include_item.to_s.include?('.')

include_item.to_s.split('.').map!(&:to_sym)
end

def remaining_items(items)
return unless items.size > 1

[items[1..-1].join('.').to_sym]
end

# includes handler
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
return unless includes_list.present?

includes_list.sort.each_with_object([]) do |include_item, included_records|
items = parse_include_item(include_item)
remaining_items = remaining_items(items)

items.each do |item|
next unless relationships_to_serialize && relationships_to_serialize[item]
relationship_item = relationships_to_serialize[item]
next unless relationship_item.include_relationship?(record, params)
unless relationship_item.polymorphic.is_a?(Hash)
record_type = relationship_item.record_type
serializer = relationship_item.serializer.to_s.constantize
end
relationship_type = relationship_item.relationship_type
includes_list.each_with_object([]) do |(item, subitems), included_records|
relationship_item = relationships_to_serialize[item]
raise FastJsonapi::InvalidIncludeError, "Could not find included relationship '#{item}' on #{self.class.name}" if relationship_item.nil?
next unless relationship_item.include_relationship?(record, params)
unless relationship_item.polymorphic.is_a?(Hash)
record_type = relationship_item.record_type
serializer = relationship_item.serializer.to_s.constantize
end
relationship_type = relationship_item.relationship_type

included_objects = relationship_item.fetch_associated_object(record, params)
next if included_objects.blank?
included_objects = [included_objects] unless relationship_type == :has_many
included_objects = relationship_item.fetch_associated_object(record, params)
next if included_objects.blank?
included_objects = [included_objects] unless relationship_type == :has_many

included_objects.each do |inc_obj|
if relationship_item.polymorphic.is_a?(Hash)
record_type = inc_obj.class.name.demodulize.underscore
serializer = self.compute_serializer_name(inc_obj.class.name.demodulize.to_sym).to_s.constantize
end
included_objects.each do |inc_obj|
if relationship_item.polymorphic.is_a?(Hash)
record_type = inc_obj.class.name.demodulize.underscore
serializer = self.compute_serializer_name(inc_obj.class.name.demodulize.to_sym).to_s.constantize
end

if remaining_items.present?
serializer_records = serializer.get_included_records(inc_obj, remaining_items, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end
if subitems.present?
serializer_records = serializer.get_included_records(inc_obj, subitems, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end

code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
next if known_included_objects.key?(code)
code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
next if known_included_objects.key?(code)

known_included_objects[code] = inc_obj
known_included_objects[code] = inc_obj

included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], includes_list, params)
end
included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], includes_list, params)
end
end
end
Expand Down
69 changes: 69 additions & 0 deletions spec/lib/object_serializer_include_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require 'spec_helper'

describe FastJsonapi::ObjectSerializer do
include_context 'movie class'

def has_included_type?(hash, type)
hash[:included].any? { |i| i[:type].to_sym == type }
end

describe '`include`' do
it 'can defined as symbols' do
hash = MovieSerializer.new(movie, include: [:actors, :advertising_campaign]).serializable_hash
expect(has_included_type?(hash, :actor)).to eq true
expect(has_included_type?(hash, :advertising_campaign)).to eq true
expect(has_included_type?(hash, :agency)).to eq false
end

it 'can be defined as strings' do
hash = MovieSerializer.new(movie, include: ['actors', 'advertising_campaign']).serializable_hash
expect(has_included_type?(hash, :actor)).to eq true
expect(has_included_type?(hash, :advertising_campaign)).to eq true
expect(has_included_type?(hash, :agency)).to eq false
end

describe 'nested relationships' do

it 'can be defined as dot notation' do
hash = MovieSerializer.new(movie, include: ['actors', 'actors.agency', 'actors.agency.state']).serializable_hash
expect(has_included_type?(hash, :actor)).to eq true
expect(has_included_type?(hash, :agency)).to eq true
expect(has_included_type?(hash, :state)).to eq true
end

it 'does not require the parent to be specified separately in dot notation' do
hash = MovieSerializer.new(movie, include: ['actors.agency.state']).serializable_hash
expect(has_included_type?(hash, :actor)).to eq true
expect(has_included_type?(hash, :agency)).to eq true
expect(has_included_type?(hash, :state)).to eq true
end

it 'can be specified as hashes' do
hash = MovieSerializer.new(movie, include: { actors: { agency: :state } }).serializable_hash
expect(has_included_type?(hash, :actor)).to eq true
expect(has_included_type?(hash, :agency)).to eq true
expect(has_included_type?(hash, :state)).to eq true
end
end

it 'can include any combination of notation' do
hash = MovieSerializer.new(movie, include: [{ actors: :agency }, :advertising_campaign, 'actors.agency.state' ]).serializable_hash
expect(has_included_type?(hash, :actor)).to eq true
expect(has_included_type?(hash, :agency)).to eq true
expect(has_included_type?(hash, :state)).to eq true
expect(has_included_type?(hash, :advertising_campaign)).to eq true
end
end

describe 'validation' do
it 'raises an exception if an included item is not a relationship on the object' do
expect { MovieSerializer.new(movie, include: [:foo]).serializable_hash }.to raise_error(FastJsonapi::InvalidIncludeError)
expect { MovieSerializer.new(movie, include: { actors: :foo }).serializable_hash }.to raise_error(FastJsonapi::InvalidIncludeError)
end

it 'properly validates dynamic (e.g. polymorphic) relationships' do
expect { TheaterSerializer.new(theater, include: { snacks: :supplier }).serializable_hash }.to_not raise_error
expect { TheaterSerializer.new(theater, include: { snacks: :foo }).serializable_hash }.to raise_error(FastJsonapi::InvalidIncludeError)
end
end
end
2 changes: 1 addition & 1 deletion spec/lib/serialization_core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
end

it 'serializes known included records only once' do
includes_list = [:actors]
includes_list = { actors: {} }
known_included_objects = {}
included_records = []
[movie, movie].each do |record|
Expand Down
58 changes: 58 additions & 0 deletions spec/shared/contexts/movie_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,30 @@ class Owner
attr_accessor :id
end

class Theater
attr_accessor :id, :name, :snacks
end

class Popcorn
attr_accessor :id, :supplier_id

def supplier
Supplier.new.tap do |s|
s.id = supplier_id
end
end
end

class Candy
attr_accessor :id, :supplier_id

def supplier
Supplier.new.tap do |s|
s.id = supplier_id
end
end
end

class OwnerSerializer
include FastJsonapi::ObjectSerializer
end
Expand Down Expand Up @@ -312,6 +336,24 @@ class AccountSerializer
belongs_to :supplier
end

class TheaterSerializer
include FastJsonapi::ObjectSerializer
set_type :theater
has_many :snacks, polymorphic: true
end

class PopcornSerializer
include FastJsonapi::ObjectSerializer
set_type :popcorn
belongs_to :supplier
end

class CandySerializer
include FastJsonapi::ObjectSerializer
set_type :candy
belongs_to :supplier
end

class MovieOptionalRecordDataSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
Expand Down Expand Up @@ -476,6 +518,22 @@ class MovieSerializer
s
end

let(:theater) do
t = Theater.new
t.id = 1
t.snacks = [
Popcorn.new.tap do |p|
p.id = 3
p.supplier_id = 4
end,
Candy.new.tap do |p|
p.id = 7
p.supplier_id = 5
end
]
t
end

def build_movies(count)
count.times.map do |i|
m = Movie.new
Expand Down