Skip to content

Commit

Permalink
Merge pull request RestPack#1 from camallen/include_links
Browse files Browse the repository at this point in the history
add support for has_an_belongs_to_many relations
  • Loading branch information
aaronbhansen committed Sep 12, 2014
2 parents e75a984 + 95e86a6 commit 86599c5
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 90 deletions.
21 changes: 10 additions & 11 deletions lib/restpack_serializer/serializable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require_relative "serializable/resource"
require_relative "serializable/single"
require_relative "serializable/side_loading"
require_relative "serializable/side_load_data_builder"
require_relative "serializable/symbolizer"
require_relative "serializable/sortable"

Expand Down Expand Up @@ -65,17 +66,15 @@ def add_custom_attributes(data)

def add_links(model, data)
self.class.associations.each do |association|
if association.macro == :belongs_to
data[:links] ||= {}
foreign_key_value = model.send(association.foreign_key)
if foreign_key_value
data[:links][association.name.to_sym] = foreign_key_value.to_s
end
elsif association.macro == :has_many
ids = model.send(association.name).pluck(:id).map { |id| id.to_s }

data[:links] ||= {}
data[:links][association.name.to_sym] = ids
data[:links] ||= {}
links_value = case
when association.macro == :belongs_to
model.send(association.foreign_key).try(:to_s)
when association.macro.to_s.match(/has_/)
model.send(association.name).pluck(:id).map(&:to_s)
end
unless links_value.blank?
data[:links][association.name.to_sym] = links_value
end
end
data
Expand Down
56 changes: 56 additions & 0 deletions lib/restpack_serializer/serializable/side_load_data_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module RestPack
module Serializer
class SideLoadDataBuilder

def initialize(association, models, serializer)
@association = association
@models = models
@serializer = serializer
end

def side_load_belongs_to
foreign_keys = @models.map { |model| model.send(@association.foreign_key) }.uniq
side_load = @association.klass.find(foreign_keys)
json_model_data = side_load.map { |model| @serializer.as_json(model) }
{ @association.plural_name.to_sym => json_model_data, meta: { } }
end

def side_load_has_many
has_association_relation do |options|
if join_table = @association.options[:through]
options.scope = options.scope.joins(join_table)
association_fk = @association.through_reflection.foreign_key.to_sym
options.filters = { join_table => { association_fk => model_ids } }
else
options.filters = { @association.foreign_key.to_sym => model_ids }
end
end
end

def side_load_has_and_belongs_to_many
has_association_relation do |options|
join_table_name = @association.join_table
join_clause = "join #{join_table_name} on #{@association.plural_name}.id = #{join_table_name}.#{@association.class_name.foreign_key}"
options.scope = options.scope.joins(join_clause)
association_fk = @association.foreign_key.to_sym
options.filters = { join_table_name.to_sym => { association_fk => model_ids } }
end
end

private

def model_ids
@models.map(&:id)
end

def has_association_relation
return {} if @models.empty?
serializer_class = @serializer.class
options = RestPack::Serializer::Options.new(serializer_class)
yield options
options.include_links = false
serializer_class.page_with_options(options)
end
end
end
end
113 changes: 40 additions & 73 deletions lib/restpack_serializer/serializable/side_loading.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ module RestPack::Serializer::SideLoading

module ClassMethods
def side_loads(models, options)
side_loads = {
:meta => { }
}
return side_loads if models.empty? || options.include.nil?
{ meta: { } }.tap do |side_loads|
return side_loads if models.empty? || options.include.nil?

options.include.each do |include|
side_load_data = side_load(include, models, options)
side_loads[:meta].merge!(side_load_data[:meta] || {})
side_loads.merge! side_load_data.except(:meta)
options.include.each do |include|
side_load_data = side_load(include, models, options)
side_loads[:meta].merge!(side_load_data[:meta] || {})
side_loads.merge! side_load_data.except(:meta)
end
end
side_loads
end

def can_includes
Expand All @@ -26,91 +24,60 @@ def can_include(*includes)
end

def links
links = {}

associations.each do |association|
if association.macro == :belongs_to
link_key = "#{self.key}.#{association.name}"
href = "/#{association.plural_name}/{#{link_key}}"
elsif association.macro == :has_many
singular_key = self.key.to_s.singularize
link_key = "#{self.key}.#{association.plural_name}"
href = "/#{association.plural_name}?#{singular_key}_id={#{key}.id}"
{}.tap do |links|
associations.each do |association|
if association.macro == :belongs_to
link_key = "#{self.key}.#{association.name}"
href = "/#{association.plural_name}/{#{link_key}}"
elsif association.macro.to_s.match(/has_/)
singular_key = self.key.to_s.singularize
link_key = "#{self.key}.#{association.plural_name}"
href = "/#{association.plural_name}?#{singular_key}_id={#{key}.id}"
end
links.merge!(link_key => {
:href => RestPack::Serializer.config.href_prefix + href,
:type => association.plural_name.to_sym
}
)
end

links[link_key] = {
:href => RestPack::Serializer.config.href_prefix + href,
:type => association.plural_name.to_sym
}
end

links
end

def associations
associations = []
can_includes.each do |include|
can_includes.map do |include|
association = association_from_include(include)
associations << association if supported_association?(association)
end
associations
association if supported_association?(association.macro)
end.compact
end

private

def side_load(include, models, options)
association = association_from_include(include)

if supported_association?(association)
serializer = RestPack::Serializer::Factory.create(association.class_name)
return send("side_load_#{association.macro}", association, models, serializer)
else
return {}
end
end

def supported_association?(association)
[:belongs_to, :has_many].include?(association.macro)
end

def side_load_belongs_to(association, models, serializer)
foreign_keys = models.map { |model| model.send(association.foreign_key) }.uniq
side_load = association.klass.find(foreign_keys)

return {
association.plural_name.to_sym => side_load.map { |model| serializer.as_json(model) },
:meta => { }
}
return {} unless supported_association?(association.macro)
serializer = RestPack::Serializer::Factory.create(association.class_name)
builder = RestPack::Serializer::SideLoadDataBuilder.new(association,
models,
serializer)
builder.send("side_load_#{association.macro}")
end

def side_load_has_many(association, models, serializer)
return {} if models.empty?

join_table = association.options[:through]

filters = if join_table
{ join_table => { association.through_reflection.foreign_key.to_sym => models.map(&:id) } }
else
{ association.foreign_key.to_sym => models.map(&:id) }
end

options = RestPack::Serializer::Options.new(serializer.class)
options.scope = options.scope.joins(join_table) if join_table
options.filters = filters
options.include_links = false

serializer.class.page_with_options(options)
def supported_association?(association_macro)
[:belongs_to, :has_many, :has_and_belongs_to_many].include?(association_macro)
end

def association_from_include(include)
raise_invalid_include(include) unless self.can_includes.include?(include)

possible_relations = [include.to_s.singularize.to_sym, include]
select_association_from_possibles(possible_relations)
end

def select_association_from_possibles(possible_relations)
possible_relations.each do |relation|
association = self.model_class.reflect_on_association(relation)
return association unless association.nil?
if association = self.model_class.reflect_on_association(relation)
return association
end
end

raise_invalid_include(include)
end

Expand Down
17 changes: 17 additions & 0 deletions spec/fixtures/db.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@
t.datetime "created_at"
t.datetime "updated_at"
end

create_table "stalkers", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end

create_table "artists_stalkers", force: true, id: false do |t|
t.integer :artist_id
t.integer :stalker_id
end
end

module MyApp
Expand All @@ -61,6 +72,7 @@ class Artist < ActiveRecord::Base
has_many :songs
has_many :payments
has_many :fans, :through => :payments
has_and_belongs_to_many :stalkers
end

class Album < ActiveRecord::Base
Expand Down Expand Up @@ -98,4 +110,9 @@ class Fan < ActiveRecord::Base
has_many :payments
has_many :artists, :through => :albums
end

class Stalker < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :artists
end
end
7 changes: 6 additions & 1 deletion spec/fixtures/serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ class AlbumReviewSerializer
class ArtistSerializer
include RestPack::Serializer
attributes :id, :name, :website
can_include :albums, :songs, :fans
can_include :albums, :songs, :fans, :stalkers
end

class FanSerializer
include RestPack::Serializer
attributes :id, :name
end

class StalkerSerializer
include RestPack::Serializer
attributes :id, :name
end
end
23 changes: 18 additions & 5 deletions spec/serializable/serializer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,27 @@ def custom_attributes
end
end

context "'has_many, through' associations" do
context "with a serializer with has_* associations" do
let(:artist_serializer) { MyApp::ArtistSerializer.new }
let(:json) { artist_serializer.as_json(artist_factory) }
let(:side_load_ids) { artist_has_association.map {|obj| obj.id.to_s } }

it "includes 'links' data when there are associated records" do
artist_with_fans = FactoryGirl.create :artist_with_fans
describe "'has_many, through' associations" do
let(:artist_factory) { FactoryGirl.create :artist_with_fans }
let(:artist_has_association) { artist_factory.fans }

json = artist_serializer.as_json(artist_with_fans)
json[:links][:fans].should == artist_with_fans.fans.collect {|obj| obj.id.to_s }
it "includes 'links' data when there are associated records" do
expect(json[:links][:fans]).to match_array(side_load_ids)
end
end

describe "'has_and_belongs_to_many' associations" do
let(:artist_factory) { FactoryGirl.create :artist_with_stalkers }
let(:artist_has_association) { artist_factory.stalkers }

it "includes 'links' data when there are associated records" do
expect(json[:links][:stalkers]).to match_array(side_load_ids)
end
end
end
end
Expand Down
44 changes: 44 additions & 0 deletions spec/serializable/side_loading/has_and_belongs_many_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'spec_helper'

describe RestPack::Serializer::SideLoading do
context "when side-loading" do
let(:side_loads) { MyApp::ArtistSerializer.side_loads(models, options) }

describe ".has_and_belongs_to_many" do

before(:each) do
@artist1 = FactoryGirl.create(:artist_with_stalkers, stalker_count: 2)
@artist2 = FactoryGirl.create(:artist_with_stalkers, stalker_count: 3)
end

context "with a single model" do
let(:models) { [@artist1] }

context "when including :albums" do
let(:options) { RestPack::Serializer::Options.new(MyApp::ArtistSerializer, { "include" => "stalkers" }) }
let(:stalker_count) { @artist1.stalkers.count }

it "returns side-loaded albums" do
side_loads[:stalkers].count.should == stalker_count
side_loads[:meta][:stalkers][:page].should == 1
side_loads[:meta][:stalkers][:count].should == stalker_count
end
end
end

context "with two models" do
let(:models) { [@artist1, @artist2] }

context "when including :albums" do
let(:options) { RestPack::Serializer::Options.new(MyApp::ArtistSerializer, { "include" => "stalkers" }) }
let(:stalker_count) { @artist1.stalkers.count + @artist2.stalkers.count }

it "returns side-loaded albums" do
side_loads[:stalkers].count.should == stalker_count
side_loads[:meta][:stalkers][:count].should == stalker_count
end
end
end
end
end
end
13 changes: 13 additions & 0 deletions spec/support/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@
create_list(:payment, evaluator.fans_count, artist: artist)
end
end

factory :artist_with_stalkers do
ignore do
stalker_count 2
end
after(:create) do |artist, evaluator|
create_list(:stalker, evaluator.stalker_count, artists: [ artist ])
end
end
end

factory :album, :class => MyApp::Album do
Expand Down Expand Up @@ -56,4 +65,8 @@
factory :fan, :class => MyApp::Fan do
sequence(:name) {|n| "Fan ##{n}"}
end

factory :stalker, :class => MyApp::Stalker do
sequence(:name) {|n| "Stalker ##{n}"}
end
end

0 comments on commit 86599c5

Please sign in to comment.