Skip to content

Commit 01b8a86

Browse files
committed
Merge pull request #304 from cerebris/resource_linkage
Resource linkage
2 parents e42f5a5 + 9b725e5 commit 01b8a86

File tree

10 files changed

+424
-194
lines changed

10 files changed

+424
-194
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,11 @@ JSONAPI.configure do |config|
12771277
# catch this error and render a 403 status code, you should add
12781278
# the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.
12791279
config.exception_class_whitelist = []
1280+
1281+
# Resource Linkage
1282+
# Controls the serialization of resource linkage for non compound documents
1283+
# NOTE: always_include_has_many_linkage_data is not currently implemented
1284+
config.always_include_has_one_linkage_data = false
12801285
end
12811286
```
12821287

lib/jsonapi/configuration.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ class Configuration
1717
:top_level_links_include_pagination,
1818
:top_level_meta_include_record_count,
1919
:top_level_meta_record_count_key,
20-
:exception_class_whitelist
20+
:exception_class_whitelist,
21+
:always_include_to_one_linkage_data,
22+
:always_include_to_many_linkage_data
2123

2224
def initialize
2325
#:underscored_key, :camelized_key, :dasherized_key, or custom
@@ -54,6 +56,12 @@ def initialize
5456
# catch this error and render a 403 status code, you should add
5557
# the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.
5658
self.exception_class_whitelist = []
59+
60+
# Resource Linkage
61+
# Controls the serialization of resource linkage for non compound documents
62+
# NOTE: always_include_to_many_linkage_data is not currently implemented
63+
self.always_include_to_one_linkage_data = false
64+
self.always_include_to_many_linkage_data = false
5765
end
5866

5967
def json_key_format=(format)
@@ -88,6 +96,10 @@ def operations_processor=(operations_processor)
8896
attr_writer :top_level_meta_record_count_key
8997

9098
attr_writer :exception_class_whitelist
99+
100+
attr_writer :always_include_to_one_linkage_data
101+
102+
attr_writer :always_include_to_many_linkage_data
91103
end
92104

93105
class << self

lib/jsonapi/relationship.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module JSONAPI
22
class Relationship
33
attr_reader :acts_as_set, :foreign_key, :type, :options, :name,
4-
:class_name, :polymorphic
4+
:class_name, :polymorphic, :always_include_linkage_data
55

66
def initialize(name, options = {})
77
@name = name.to_s
@@ -11,6 +11,7 @@ def initialize(name, options = {})
1111
@module_path = options[:module_path] || ''
1212
@relation_name = options.fetch(:relation_name, @name)
1313
@polymorphic = options.fetch(:polymorphic, false) == true
14+
@always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
1415
end
1516

1617
alias_method :polymorphic?, :polymorphic

lib/jsonapi/resource.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ def check_reserved_resource_name(type, name)
653653
def check_reserved_attribute_name(name)
654654
# Allow :id since it can be used to specify the format. Since it is a method on the base Resource
655655
# an attribute method won't be created for it.
656-
if [:type, :href, :links].include?(name.to_sym)
656+
if [:type, :href, :links, :model].include?(name.to_sym)
657657
warn "[NAME COLLISION] `#{name}` is a reserved key in #{@@resource_types[_type]}."
658658
end
659659
end

lib/jsonapi/resource_serializer.rb

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def initialize(primary_resource_klass, options = {})
2121
@include_directives = options[:include_directives]
2222
@key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
2323
@url_generator = generate_link_builder(primary_resource_klass, options)
24+
@always_include_to_one_linkage_data = options.fetch(:always_include_to_one_linkage_data,
25+
JSONAPI.configuration.always_include_to_one_linkage_data)
26+
@always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
27+
JSONAPI.configuration.always_include_to_many_linkage_data)
2428
end
2529

2630
# Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
@@ -80,18 +84,18 @@ def process_primary(source, include_directives)
8084
if source.respond_to?(:to_ary)
8185
source.each do |resource|
8286
id = resource.id
83-
if already_serialized?(@primary_class_name, id)
87+
if already_serialized?(resource.class._type, id)
8488
set_primary(@primary_class_name, id)
8589
end
8690

87-
add_included_object(@primary_class_name, id, object_hash(resource, include_directives), true)
91+
add_included_object(id, object_hash(resource, include_directives), true)
8892
end
8993
else
9094
return {} if source.nil?
9195

9296
resource = source
9397
id = resource.id
94-
add_included_object(@primary_class_name, id, object_hash(source, include_directives), true)
98+
add_included_object(id, object_hash(source, include_directives), true)
9599
end
96100
end
97101

@@ -171,7 +175,7 @@ def relationship_data(source, include_directives)
171175
type = relationship.type_for_source(source)
172176
relationships_only = already_serialized?(type, id)
173177
if include_linkage && !relationships_only
174-
add_included_object(type, id, object_hash(resource, ia))
178+
add_included_object(id, object_hash(resource, ia))
175179
elsif include_linked_children || relationships_only
176180
relationship_data(resource, ia)
177181
end
@@ -182,7 +186,7 @@ def relationship_data(source, include_directives)
182186
id = resource.id
183187
relationships_only = already_serialized?(type, id)
184188
if include_linkage && !relationships_only
185-
add_included_object(type, id, object_hash(resource, ia))
189+
add_included_object(id, object_hash(resource, ia))
186190
elsif include_linked_children || relationships_only
187191
relationship_data(resource, ia)
188192
end
@@ -236,12 +240,13 @@ def to_many_linkage(source, relationship)
236240
linkage
237241
end
238242

239-
def link_object_to_one(source, relationship)
243+
def link_object_to_one(source, relationship, include_linkage)
244+
include_linkage = include_linkage | @always_include_to_one_linkage_data | relationship.always_include_linkage_data
240245
link_object_hash = {}
241246
link_object_hash[:links] = {}
242247
link_object_hash[:links][:self] = self_link(source, relationship)
243248
link_object_hash[:links][:related] = related_link(source, relationship)
244-
link_object_hash[:data] = to_one_linkage(source, relationship)
249+
link_object_hash[:data] = to_one_linkage(source, relationship) if include_linkage
245250
link_object_hash
246251
end
247252

@@ -256,7 +261,7 @@ def link_object_to_many(source, relationship, include_linkage)
256261

257262
def link_object(source, relationship, include_linkage = false)
258263
if relationship.is_a?(JSONAPI::Relationship::ToOne)
259-
link_object_to_one(source, relationship)
264+
link_object_to_one(source, relationship, include_linkage)
260265
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
261266
link_object_to_many(source, relationship, include_linkage)
262267
end
@@ -290,12 +295,13 @@ def set_primary(type, id)
290295
end
291296

292297
# Collects the hashes for all objects processed by the serializer
293-
def add_included_object(type, id, object_hash, primary = false)
294-
type = format_key(type)
298+
def add_included_object(id, object_hash, primary = false)
299+
type = object_hash['type']
295300

296301
@included_objects[type] = {} unless @included_objects.key?(type)
297302

298303
if already_serialized?(type, id)
304+
@included_objects[type][id][:object_hash].merge!(object_hash)
299305
set_primary(type, id) if primary
300306
else
301307
@included_objects[type].store(id, primary: primary, object_hash: object_hash)

test/controllers/controller_test.rb

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ def test_show_single_with_fields
275275
assert_response :success
276276
assert json_response['data'].is_a?(Hash)
277277
assert_nil json_response['data']['attributes']
278-
assert_equal '1', json_response['data']['relationships']['author']['data']['id']
279278
end
280279

281280
def test_show_single_with_fields_string
@@ -326,7 +325,6 @@ def test_create_simple
326325

327326
assert_response :created
328327
assert json_response['data'].is_a?(Hash)
329-
assert_equal '3', json_response['data']['relationships']['author']['data']['id']
330328
assert_equal 'JR is Great', json_response['data']['attributes']['title']
331329
assert_equal 'JSONAPIResources is the greatest thing since unsliced bread.', json_response['data']['attributes']['body']
332330
end
@@ -431,7 +429,7 @@ def test_create_multiple
431429
assert_response :created
432430
assert json_response['data'].is_a?(Array)
433431
assert_equal json_response['data'].size, 2
434-
assert_equal json_response['data'][0]['relationships']['author']['data']['id'], '3'
432+
assert_nil json_response['data'][0]['relationships']['author']['data']
435433
assert_match /JR is Great/, response.body
436434
assert_match /Ember is Great/, response.body
437435
end
@@ -561,7 +559,8 @@ def test_create_with_links_to_many_type_ids
561559
author: {data: {type: 'people', id: '3'}},
562560
tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
563561
}
564-
}
562+
},
563+
include: 'author'
565564
}
566565

567566
assert_response :created
@@ -585,7 +584,8 @@ def test_create_with_links_to_many_array
585584
author: {data: {type: 'people', id: '3'}},
586585
tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
587586
}
588-
}
587+
},
588+
include: 'author'
589589
}
590590

591591
assert_response :created
@@ -639,7 +639,7 @@ def test_update_with_links
639639
tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
640640
}
641641
},
642-
include: 'tags'
642+
include: 'tags,author,section'
643643
}
644644

645645
assert_response :success
@@ -709,7 +709,7 @@ def test_update_remove_links
709709
tags: []
710710
}
711711
},
712-
include: 'tags'
712+
include: 'tags,author,section'
713713
}
714714

715715
assert_response :success
@@ -1246,15 +1246,11 @@ def test_update_multiple
12461246

12471247
assert_response :success
12481248
assert_equal json_response['data'].size, 2
1249-
assert_equal json_response['data'][0]['relationships']['author']['data']['id'], '3'
1250-
assert_equal json_response['data'][0]['relationships']['section']['data']['id'], javascript.id.to_s
12511249
assert_equal json_response['data'][0]['attributes']['title'], 'A great new Post QWERTY'
12521250
assert_equal json_response['data'][0]['attributes']['body'], 'AAAA'
12531251
assert matches_array?([{'type' => 'tags', 'id' => '3'}, {'type' => 'tags', 'id' => '4'}],
12541252
json_response['data'][0]['relationships']['tags']['data'])
12551253

1256-
assert_equal json_response['data'][1]['relationships']['author']['data']['id'], '3'
1257-
assert_equal json_response['data'][1]['relationships']['section']['data']['id'], javascript.id.to_s
12581254
assert_equal json_response['data'][1]['attributes']['title'], 'A great new Post ASDFG'
12591255
assert_equal json_response['data'][1]['attributes']['body'], 'Not First!!!!'
12601256
assert matches_array?([{'type' => 'tags', 'id' => '3'}, {'type' => 'tags', 'id' => '4'}],
@@ -1601,7 +1597,7 @@ def test_create_expense_entries_underscored
16011597
iso_currency: {data: {type: 'iso_currencies', id: 'USD'}}
16021598
}
16031599
},
1604-
include: 'iso_currency',
1600+
include: 'iso_currency,employee',
16051601
fields: {expense_entries: 'id,transaction_date,iso_currency,cost,employee'}
16061602
}
16071603

@@ -1632,7 +1628,7 @@ def test_create_expense_entries_camelized_key
16321628
isoCurrency: {data: {type: 'iso_currencies', id: 'USD'}}
16331629
}
16341630
},
1635-
include: 'isoCurrency',
1631+
include: 'isoCurrency,employee',
16361632
fields: {expenseEntries: 'id,transactionDate,isoCurrency,cost,employee'}
16371633
}
16381634

@@ -1663,7 +1659,7 @@ def test_create_expense_entries_dasherized_key
16631659
'iso-currency' => {data: {type: 'iso_currencies', id: 'USD'}}
16641660
}
16651661
},
1666-
include: 'iso-currency',
1662+
include: 'iso-currency,employee',
16671663
fields: {'expense-entries' => 'id,transaction-date,iso-currency,cost,employee'}
16681664
}
16691665

@@ -1948,18 +1944,13 @@ def test_get_related_resource
19481944
links: {
19491945
self: 'http://test.host/people/1/relationships/preferences',
19501946
related: 'http://test.host/people/1/preferences'
1951-
},
1952-
data: {
1953-
type: 'preferences',
1954-
id: '1'
19551947
}
19561948
},
19571949
"hair-cut" => {
19581950
"links" => {
19591951
"self" => "http://test.host/people/1/relationships/hair_cut",
19601952
"related" => "http://test.host/people/1/hair_cut"
1961-
},
1962-
"data" => nil
1953+
}
19631954
},
19641955
vehicles: {
19651956
links: {
@@ -2160,7 +2151,6 @@ def test_create_simple_namespaced
21602151

21612152
assert_response :created
21622153
assert json_response['data'].is_a?(Hash)
2163-
assert_equal '3', json_response['data']['relationships']['writer']['data']['id']
21642154
assert_equal 'JR - now with Namespacing', json_response['data']['attributes']['title']
21652155
assert_equal 'JSONAPIResources is the greatest thing since unsliced bread now that it has namespaced resources.',
21662156
json_response['data']['attributes']['body']

test/fixtures/active_record.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@
195195

196196
create_table :vehicles, force: true do |t|
197197
t.string :type
198+
t.string :make
199+
t.string :vehicle_model
200+
t.string :length_at_water_line
201+
t.string :drive_layout
202+
t.string :serial_number
198203
t.integer :person_id
199204
end
200205
end
@@ -644,12 +649,15 @@ def self.verify_custom_filter(filter, values, context)
644649

645650
class VehicleResource < JSONAPI::Resource
646651
has_one :person
652+
attributes :make, :vehicle_model, :serial_number
647653
end
648654

649655
class CarResource < VehicleResource
656+
attributes :drive_layout
650657
end
651658

652659
class BoatResource < VehicleResource
660+
attributes :length_at_water_line
653661
end
654662

655663
class CommentResource < JSONAPI::Resource
@@ -893,7 +901,7 @@ class DocumentResource < JSONAPI::Resource
893901

894902
class ProductResource < JSONAPI::Resource
895903
attribute :name
896-
has_one :picture
904+
has_one :picture, always_include_linkage_data: true
897905

898906
def picture_id
899907
model.picture.id

test/fixtures/vehicles.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
car:
1+
Miata:
22
id: 1
33
type: Car
4+
make: Mazda
5+
vehicle_model: Miata MX5
6+
drive_layout: Front Engine RWD
7+
serial_number: 32432adfsfdysua
48
person_id: 1
5-
boat:
9+
Launch20:
610
id: 2
711
type: Boat
12+
make: Chris-Craft
13+
vehicle_model: Launch 20
14+
length_at_water_line: 15.5ft
15+
serial_number: 434253JJJSD
816
person_id: 1

0 commit comments

Comments
 (0)