jsonapi-resources 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +274 -102
  3. data/jsonapi-resources.gemspec +1 -0
  4. data/lib/jsonapi-resources.rb +15 -0
  5. data/lib/jsonapi/active_record_operations_processor.rb +21 -10
  6. data/lib/jsonapi/acts_as_resource_controller.rb +175 -0
  7. data/lib/jsonapi/configuration.rb +11 -0
  8. data/lib/jsonapi/error_codes.rb +7 -4
  9. data/lib/jsonapi/exceptions.rb +23 -15
  10. data/lib/jsonapi/formatter.rb +5 -5
  11. data/lib/jsonapi/include_directives.rb +67 -0
  12. data/lib/jsonapi/operation.rb +185 -65
  13. data/lib/jsonapi/operation_result.rb +38 -5
  14. data/lib/jsonapi/operation_results.rb +33 -0
  15. data/lib/jsonapi/operations_processor.rb +49 -9
  16. data/lib/jsonapi/paginator.rb +31 -17
  17. data/lib/jsonapi/request.rb +347 -163
  18. data/lib/jsonapi/resource.rb +159 -56
  19. data/lib/jsonapi/resource_controller.rb +1 -234
  20. data/lib/jsonapi/resource_serializer.rb +55 -69
  21. data/lib/jsonapi/resources/version.rb +1 -1
  22. data/lib/jsonapi/response_document.rb +87 -0
  23. data/lib/jsonapi/routing_ext.rb +17 -11
  24. data/test/controllers/controller_test.rb +602 -326
  25. data/test/fixtures/active_record.rb +96 -6
  26. data/test/fixtures/line_items.yml +7 -1
  27. data/test/fixtures/numeros_telefone.yml +3 -0
  28. data/test/fixtures/purchase_orders.yml +6 -0
  29. data/test/integration/requests/request_test.rb +129 -60
  30. data/test/integration/routes/routes_test.rb +17 -17
  31. data/test/test_helper.rb +23 -5
  32. data/test/unit/jsonapi_request/jsonapi_request_test.rb +48 -0
  33. data/test/unit/operation/operations_processor_test.rb +242 -54
  34. data/test/unit/resource/resource_test.rb +108 -2
  35. data/test/unit/serializer/include_directives_test.rb +108 -0
  36. data/test/unit/serializer/response_document_test.rb +61 -0
  37. data/test/unit/serializer/serializer_test.rb +679 -520
  38. metadata +26 -2
@@ -3,6 +3,7 @@ require 'jsonapi-resources'
3
3
 
4
4
  ActiveSupport::Inflector.inflections(:en) do |inflect|
5
5
  inflect.uncountable 'preferences'
6
+ inflect.irregular 'numero_telefone', 'numeros_telefone'
6
7
  end
7
8
 
8
9
  ### DATABASE
@@ -156,6 +157,16 @@ ActiveRecord::Schema.define do
156
157
  create_table :hair_cuts, force: true do |t|
157
158
  t.string :style
158
159
  end
160
+
161
+ create_table :numeros_telefone, force: true do |t|
162
+ t.string :numero_telefone
163
+ t.timestamps null: false
164
+ end
165
+
166
+ create_table :categories, force: true do |t|
167
+ t.string :name
168
+ t.string :status, limit: 10
169
+ end
159
170
  end
160
171
 
161
172
  ### MODELS
@@ -211,6 +222,16 @@ class Planet < ActiveRecord::Base
211
222
  belongs_to :planet_type
212
223
 
213
224
  has_and_belongs_to_many :tags, join_table: :planets_tags
225
+
226
+ # Test model callback cancelling save
227
+ before_save :check_not_pluto
228
+
229
+ def check_not_pluto
230
+ # Pluto can't be a planet, so cancel the save
231
+ if name.downcase == 'pluto'
232
+ return false
233
+ end
234
+ end
214
235
  end
215
236
 
216
237
  class PlanetType < ActiveRecord::Base
@@ -242,6 +263,7 @@ class Breed
242
263
  @id = id
243
264
  end
244
265
  @name = name
266
+ @errors = ActiveModel::Errors.new(self)
245
267
  end
246
268
 
247
269
  attr_accessor :id, :name
@@ -250,7 +272,18 @@ class Breed
250
272
  $breed_data.remove(@id)
251
273
  end
252
274
 
253
- def save!
275
+ def valid?
276
+ @errors.clear
277
+ if name.is_a?(String) && name.length > 0
278
+ return true
279
+ else
280
+ @errors.set(:name, ["can't be blank"])
281
+ return false
282
+ end
283
+ end
284
+
285
+ def errors
286
+ @errors
254
287
  end
255
288
  end
256
289
 
@@ -304,6 +337,12 @@ class LineItem < ActiveRecord::Base
304
337
  belongs_to :purchase_order
305
338
  end
306
339
 
340
+ class NumeroTelefone < ActiveRecord::Base
341
+ end
342
+
343
+ class Category < ActiveRecord::Base
344
+ end
345
+
307
346
  ### PORO Data - don't do this in a production app
308
347
  $breed_data = BreedData.new
309
348
  $breed_data.add(Breed.new(0, 'persian'))
@@ -311,6 +350,19 @@ $breed_data.add(Breed.new(1, 'siamese'))
311
350
  $breed_data.add(Breed.new(2, 'sphinx'))
312
351
  $breed_data.add(Breed.new(3, 'to_delete'))
313
352
 
353
+ ### OperationsProcessor
354
+ class CountingActiveRecordOperationsProcessor < ActiveRecordOperationsProcessor
355
+ after_find_operation do
356
+
357
+ count = @operation.resource_klass.find_count(@operation.resource_klass.verify_filters(@operation.filters, @context),
358
+ context: @context,
359
+ include_directives: @operation.include_directives,
360
+ sort_criteria: @operation.sort_criteria)
361
+
362
+ @operation_meta[:total_records] = count
363
+ end
364
+ end
365
+
314
366
  ### CONTROLLERS
315
367
  class AuthorsController < JSONAPI::ResourceController
316
368
  end
@@ -318,7 +370,8 @@ end
318
370
  class PeopleController < JSONAPI::ResourceController
319
371
  end
320
372
 
321
- class PostsController < JSONAPI::ResourceController
373
+ class PostsController < ActionController::Base
374
+ include JSONAPI::ActsAsResourceController
322
375
  end
323
376
 
324
377
  class CommentsController < JSONAPI::ResourceController
@@ -342,6 +395,9 @@ end
342
395
  class FactsController < JSONAPI::ResourceController
343
396
  end
344
397
 
398
+ class CategoriesController < JSONAPI::ResourceController
399
+ end
400
+
345
401
  ### CONTROLLERS
346
402
  module Api
347
403
  module V1
@@ -351,7 +407,8 @@ module Api
351
407
  class PeopleController < JSONAPI::ResourceController
352
408
  end
353
409
 
354
- class PostsController < JSONAPI::ResourceController
410
+ class PostsController < ActionController::Base
411
+ include JSONAPI::ActsAsResourceController
355
412
  end
356
413
 
357
414
  class TagsController < JSONAPI::ResourceController
@@ -459,6 +516,11 @@ module Api
459
516
  class OrderFlagsController < JSONAPI::ResourceController
460
517
  end
461
518
  end
519
+
520
+ module V8
521
+ class NumerosTelefoneController < JSONAPI::ResourceController
522
+ end
523
+ end
462
524
  end
463
525
 
464
526
  ### RESOURCES
@@ -492,6 +554,8 @@ class CommentResource < JSONAPI::Resource
492
554
  has_one :post
493
555
  has_one :author, class_name: 'Person'
494
556
  has_many :tags
557
+
558
+ filters :body
495
559
  end
496
560
 
497
561
  class TagResource < JSONAPI::Resource
@@ -555,11 +619,11 @@ class PostResource < JSONAPI::Resource
555
619
  filters :title, :author, :tags, :comments
556
620
  filters :id, :ids
557
621
 
558
- def self.updateable_fields(context)
622
+ def self.updatable_fields(context)
559
623
  super(context) - [:author, :subject]
560
624
  end
561
625
 
562
- def self.createable_fields(context)
626
+ def self.creatable_fields(context)
563
627
  super(context) - [:subject]
564
628
  end
565
629
 
@@ -643,6 +707,11 @@ class BreedResource < JSONAPI::Resource
643
707
  def self.find_by_key(id, options = {})
644
708
  BreedResource.new($breed_data.breeds[id.to_i], options[:context])
645
709
  end
710
+
711
+ def _save
712
+ super
713
+ return :accepted
714
+ end
646
715
  end
647
716
 
648
717
  class PlanetResource < JSONAPI::Resource
@@ -696,6 +765,10 @@ class FactResource < JSONAPI::Resource
696
765
  attribute :cool
697
766
  end
698
767
 
768
+ class CategoryResource < JSONAPI::Resource
769
+ filter :status, default: 'active'
770
+ end
771
+
699
772
  module Api
700
773
  module V1
701
774
  class WriterResource < JSONAPI::Resource
@@ -782,6 +855,10 @@ module Api
782
855
  class BookResource < Api::V2::BookResource
783
856
  paginator :paged
784
857
  end
858
+
859
+ class BookCommentResource < Api::V2::BookCommentResource
860
+ paginator :paged
861
+ end
785
862
  end
786
863
  end
787
864
 
@@ -866,6 +943,12 @@ module Api
866
943
  OrderFlagResource = V6::OrderFlagResource.dup
867
944
  LineItemResource = V6::LineItemResource.dup
868
945
  end
946
+
947
+ module V8
948
+ class NumeroTelefoneResource < JSONAPI::Resource
949
+ attribute :numero_telefone
950
+ end
951
+ end
869
952
  end
870
953
 
871
954
  warn 'start testing Name Collisions'
@@ -896,10 +979,17 @@ saturn = Planet.create(name: 'Satern',
896
979
  description: 'Saturn is the sixth planet from the Sun and the second largest planet in the Solar System, after Jupiter.',
897
980
  planet_type_id: planetoid.id)
898
981
  titan = Moon.create(name:'Titan', description: 'Best known of the Saturn moons.', planet_id: saturn.id)
899
- pluto = Planet.create(name: 'Pluto', description: 'Pluto is the smallest planet.', planet_type_id: planetoid.id)
982
+ makemake = Planet.create(name: 'Makemake', description: 'A small planetoid in the Kuiperbelt.', planet_type_id: planetoid.id)
900
983
  uranus = Planet.create(name: 'Uranus', description: 'Insert adolescent jokes here.', planet_type_id: gas_giant.id)
901
984
  jupiter = Planet.create(name: 'Jupiter', description: 'A gas giant.', planet_type_id: gas_giant.id)
902
985
  betax = Planet.create(name: 'Beta X', description: 'Newly discovered Planet X', planet_type_id: unknown.id)
903
986
  betay = Planet.create(name: 'Beta X', description: 'Newly discovered Planet Y', planet_type_id: unknown.id)
904
987
  betaz = Planet.create(name: 'Beta X', description: 'Newly discovered Planet Z', planet_type_id: unknown.id)
905
988
  betaw = Planet.create(name: 'Beta W', description: 'Newly discovered Planet W')
989
+ Category.create(name: 'Category A', status: 'active')
990
+ Category.create(name: 'Category B', status: 'active')
991
+ Category.create(name: 'Category C', status: 'active')
992
+ Category.create(name: 'Category D', status: 'inactive')
993
+ Category.create(name: 'Category E', status: 'inactive')
994
+ Category.create(name: 'Category F', status: 'inactive')
995
+ Category.create(name: 'Category G', status: 'inactive')
@@ -22,4 +22,10 @@ li_4:
22
22
  id: 4
23
23
  part_number: 5678
24
24
  quantity: 2
25
- item_cost: 199.99
25
+ item_cost: 199.99
26
+
27
+ li_5:
28
+ id: 5
29
+ part_number: 5WERT
30
+ quantity: 1
31
+ item_cost: 299.98
@@ -0,0 +1,3 @@
1
+ info:
2
+ id: 1
3
+ numero_telefone: 1-800-555-1212
@@ -9,3 +9,9 @@ po_2:
9
9
  requested_delivery_date:
10
10
  delivery_date: nil
11
11
  customer_id: 1
12
+
13
+ po_3:
14
+ id: 3
15
+ requested_delivery_date:
16
+ delivery_date: nil
17
+ customer_id: 1
@@ -1,8 +1,6 @@
1
1
  require File.expand_path('../../../test_helper', __FILE__)
2
- require File.expand_path('../../../fixtures/active_record', __FILE__)
3
2
 
4
3
  class RequestTest < ActionDispatch::IntegrationTest
5
-
6
4
  def setup
7
5
  JSONAPI.configuration.json_key_format = :underscored_key
8
6
  JSONAPI.configuration.route_format = :underscored_route
@@ -18,6 +16,11 @@ class RequestTest < ActionDispatch::IntegrationTest
18
16
  assert_equal 200, status
19
17
  end
20
18
 
19
+ def test_get_inflected_resource
20
+ get '/api/v8/numeros_telefone'
21
+ assert_equal 200, status
22
+ end
23
+
21
24
  def test_get_nested_has_one
22
25
  get '/posts/1/author'
23
26
  assert_equal 200, status
@@ -45,7 +48,7 @@ class RequestTest < ActionDispatch::IntegrationTest
45
48
  get '/iso_currencies?filter[country_name]=Canada'
46
49
  assert_equal 200, status
47
50
  assert_equal 1, json_response['data'].size
48
- assert_equal 'Canada', json_response['data'][0]['country_name']
51
+ assert_equal 'Canada', json_response['data'][0]['attributes']['country_name']
49
52
  end
50
53
 
51
54
  def test_get_camelized_key_filtered
@@ -53,7 +56,7 @@ class RequestTest < ActionDispatch::IntegrationTest
53
56
  get '/iso_currencies?filter[countryName]=Canada'
54
57
  assert_equal 200, status
55
58
  assert_equal 1, json_response['data'].size
56
- assert_equal 'Canada', json_response['data'][0]['countryName']
59
+ assert_equal 'Canada', json_response['data'][0]['attributes']['countryName']
57
60
  end
58
61
 
59
62
  def test_get_camelized_route_and_key_filtered
@@ -61,16 +64,16 @@ class RequestTest < ActionDispatch::IntegrationTest
61
64
  get '/api/v4/isoCurrencies?filter[countryName]=Canada'
62
65
  assert_equal 200, status
63
66
  assert_equal 1, json_response['data'].size
64
- assert_equal 'Canada', json_response['data'][0]['countryName']
67
+ assert_equal 'Canada', json_response['data'][0]['attributes']['countryName']
65
68
  end
66
69
 
67
70
  def test_get_camelized_route_and_links
68
71
  JSONAPI.configuration.json_key_format = :camelized_key
69
72
  JSONAPI.configuration.route_format = :camelized_route
70
- get '/api/v4/expenseEntries/1/links/isoCurrency'
73
+ get '/api/v4/expenseEntries/1/relationships/isoCurrency'
71
74
  assert_equal 200, status
72
75
  assert_hash_equals({'links' => {
73
- 'self' => 'http://www.example.com/api/v4/expenseEntries/1/links/isoCurrency',
76
+ 'self' => 'http://www.example.com/api/v4/expenseEntries/1/relationships/isoCurrency',
74
77
  'related' => 'http://www.example.com/api/v4/expenseEntries/1/isoCurrency'
75
78
  },
76
79
  'data' => {
@@ -88,7 +91,9 @@ class RequestTest < ActionDispatch::IntegrationTest
88
91
  'type' => 'posts',
89
92
  'id' => '3',
90
93
  },
91
- 'title' => 'A great new Post',
94
+ 'attributes' => {
95
+ 'title' => 'A great new Post'
96
+ },
92
97
  'links' => {
93
98
  'tags' => [
94
99
  {type: 'tags', id: 3},
@@ -107,10 +112,12 @@ class RequestTest < ActionDispatch::IntegrationTest
107
112
  'data' => {
108
113
  'type' => 'posts',
109
114
  'id' => '3',
110
- 'title' => 'A great new Post',
111
- 'links' => {
115
+ 'attributes' => {
116
+ 'title' => 'A great new Post'
117
+ },
118
+ 'relationships' => {
112
119
  'tags' => {
113
- 'linkage' => [
120
+ 'data' => [
114
121
  {type: 'tags', id: 3},
115
122
  {type: 'tags', id: 4}
116
123
  ]
@@ -126,10 +133,12 @@ class RequestTest < ActionDispatch::IntegrationTest
126
133
  post '/posts',
127
134
  {
128
135
  'posts' => {
129
- 'title' => 'A great new Post',
130
- 'links' => {
136
+ 'attributes' => {
137
+ 'title' => 'A great new Post'
138
+ },
139
+ 'relationships' => {
131
140
  'tags' => {
132
- 'linkage' => [
141
+ 'data' => [
133
142
  {type: 'tags', id: 3},
134
143
  {type: 'tags', id: 4}
135
144
  ]
@@ -146,10 +155,12 @@ class RequestTest < ActionDispatch::IntegrationTest
146
155
  {
147
156
  'data' => {
148
157
  'type' => 'posts',
149
- 'title' => 'A great new Post',
150
- 'body' => 'JSONAPIResources is the greatest thing since unsliced bread.',
151
- 'links' => {
152
- 'author' => {'linkage' => {type: 'people', id: '3'}}
158
+ 'attributes' => {
159
+ 'title' => 'A great new Post',
160
+ 'body' => 'JSONAPIResources is the greatest thing since unsliced bread.'
161
+ },
162
+ 'relationships' => {
163
+ 'author' => {'data' => {type: 'people', id: '3'}}
153
164
  }
154
165
  }
155
166
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -176,9 +187,9 @@ class RequestTest < ActionDispatch::IntegrationTest
176
187
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
177
188
 
178
189
  assert_equal 201, status
179
- assert_nil json_response['data']['body']
180
- assert_nil json_response['data']['links']['post']['linkage']
181
- assert_nil json_response['data']['links']['author']['linkage']
190
+ assert_nil json_response['data']['attributes']['body']
191
+ assert_nil json_response['data']['relationships']['post']['data']
192
+ assert_nil json_response['data']['relationships']['author']['data']
182
193
  end
183
194
 
184
195
  def test_post_single_minimal_invalid
@@ -194,21 +205,21 @@ class RequestTest < ActionDispatch::IntegrationTest
194
205
 
195
206
  def test_update_association_without_content_type
196
207
  ruby = Section.find_by(name: 'ruby')
197
- patch '/posts/3/links/section', { 'data' => {type: 'sections', id: ruby.id.to_s }}.to_json
208
+ patch '/posts/3/relationships/section', { 'data' => {type: 'sections', id: ruby.id.to_s }}.to_json
198
209
 
199
210
  assert_equal 415, status
200
211
  end
201
212
 
202
213
  def test_patch_update_association_has_one
203
214
  ruby = Section.find_by(name: 'ruby')
204
- patch '/posts/3/links/section', { 'data' => {type: 'sections', id: ruby.id.to_s }}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
215
+ patch '/posts/3/relationships/section', { 'data' => {type: 'sections', id: ruby.id.to_s }}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
205
216
 
206
217
  assert_equal 204, status
207
218
  end
208
219
 
209
220
  def test_put_update_association_has_one
210
221
  ruby = Section.find_by(name: 'ruby')
211
- put '/posts/3/links/section', { 'data' => {type: 'sections', id: ruby.id.to_s }}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
222
+ put '/posts/3/relationships/section', { 'data' => {type: 'sections', id: ruby.id.to_s }}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
212
223
 
213
224
  assert_equal 204, status
214
225
  end
@@ -217,14 +228,14 @@ class RequestTest < ActionDispatch::IntegrationTest
217
228
  # Comments are acts_as_set=false so PUT/PATCH should respond with 403
218
229
 
219
230
  rogue = Comment.find_by(body: 'Rogue Comment Here')
220
- patch '/posts/5/links/comments', { 'data' => [{type: 'comments', id: rogue.id.to_s }]}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
231
+ patch '/posts/5/relationships/comments', { 'data' => [{type: 'comments', id: rogue.id.to_s }]}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
221
232
 
222
233
  assert_equal 403, status
223
234
  end
224
235
 
225
236
  def test_post_update_association_has_many
226
237
  rogue = Comment.find_by(body: 'Rogue Comment Here')
227
- post '/posts/5/links/comments', { 'data' => [{type: 'comments', id: rogue.id.to_s }]}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
238
+ post '/posts/5/relationships/comments', { 'data' => [{type: 'comments', id: rogue.id.to_s }]}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
228
239
 
229
240
  assert_equal 204, status
230
241
  end
@@ -233,7 +244,7 @@ class RequestTest < ActionDispatch::IntegrationTest
233
244
  # Comments are acts_as_set=false so PUT/PATCH should respond with 403. Note: JR currently treats PUT and PATCH as equivalent
234
245
 
235
246
  rogue = Comment.find_by(body: 'Rogue Comment Here')
236
- put '/posts/5/links/comments', { 'data' => [{type: 'comments', id: rogue.id.to_s }]}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
247
+ put '/posts/5/relationships/comments', { 'data' => [{type: 'comments', id: rogue.id.to_s }]}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
237
248
 
238
249
  assert_equal 403, status
239
250
  end
@@ -254,10 +265,12 @@ class RequestTest < ActionDispatch::IntegrationTest
254
265
  'data' => {
255
266
  'type' => 'posts',
256
267
  'id' => '3',
257
- 'title' => 'A great new Post',
258
- 'links' => {
268
+ 'attributes' => {
269
+ 'title' => 'A great new Post'
270
+ },
271
+ 'relationships' => {
259
272
  'tags' => {
260
- 'linkage' => [
273
+ 'data' => [
261
274
  {type: 'tags', id: 3},
262
275
  {type: 'tags', id: 4}
263
276
  ]
@@ -275,10 +288,12 @@ class RequestTest < ActionDispatch::IntegrationTest
275
288
  'data' => {
276
289
  'type' => 'posts',
277
290
  'id' => '3',
278
- 'title' => 'A great new Post',
279
- 'links' => {
291
+ 'attributes' => {
292
+ 'title' => 'A great new Post'
293
+ },
294
+ 'relationships' => {
280
295
  'tags' => {
281
- 'linkage' => [
296
+ 'data' => [
282
297
  {type: 'tags', id: 3},
283
298
  {type: 'tags', id: 4}
284
299
  ]
@@ -295,9 +310,11 @@ class RequestTest < ActionDispatch::IntegrationTest
295
310
  {
296
311
  'data' => {
297
312
  'type' => 'posts',
298
- 'title' => 'A great new Post',
299
- 'links' => {
300
- 'author' => {'linkage' => {type: 'people', id: '3'}}
313
+ 'attributes' => {
314
+ 'title' => 'A great new Post'
315
+ },
316
+ 'relationships' => {
317
+ 'author' => {'data' => {type: 'people', id: '3'}}
301
318
  }
302
319
  }
303
320
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -328,7 +345,7 @@ class RequestTest < ActionDispatch::IntegrationTest
328
345
  get '/api/v2/books'
329
346
  assert_equal 200, status
330
347
  assert_equal JSONAPI.configuration.default_page_size, json_response['data'].size
331
- assert_equal 'Book 0', json_response['data'][0]['title']
348
+ assert_equal 'Book 0', json_response['data'][0]['attributes']['title']
332
349
  end
333
350
 
334
351
  def test_pagination_offset_style_offset
@@ -336,7 +353,7 @@ class RequestTest < ActionDispatch::IntegrationTest
336
353
  get '/api/v2/books?page[offset]=50'
337
354
  assert_equal 200, status
338
355
  assert_equal JSONAPI.configuration.default_page_size, json_response['data'].size
339
- assert_equal 'Book 50', json_response['data'][0]['title']
356
+ assert_equal 'Book 50', json_response['data'][0]['attributes']['title']
340
357
  end
341
358
 
342
359
  def test_pagination_offset_style_offset_limit
@@ -344,7 +361,7 @@ class RequestTest < ActionDispatch::IntegrationTest
344
361
  get '/api/v2/books?page[offset]=50&page[limit]=20'
345
362
  assert_equal 200, status
346
363
  assert_equal 20, json_response['data'].size
347
- assert_equal 'Book 50', json_response['data'][0]['title']
364
+ assert_equal 'Book 50', json_response['data'][0]['attributes']['title']
348
365
  end
349
366
 
350
367
  def test_pagination_offset_bad_param
@@ -359,7 +376,7 @@ class RequestTest < ActionDispatch::IntegrationTest
359
376
  assert_equal 200, status
360
377
  assert_equal 2, json_response['data'].size
361
378
  assert_equal 'http://www.example.com/api/v2/books/1/book_comments',
362
- json_response['data'][1]['links']['book_comments']['related']
379
+ json_response['data'][1]['relationships']['book_comments']['links']['related']
363
380
  end
364
381
 
365
382
  def test_pagination_related_resources_data
@@ -368,7 +385,7 @@ class RequestTest < ActionDispatch::IntegrationTest
368
385
  get '/api/v2/books/1/book_comments?page[limit]=10'
369
386
  assert_equal 200, status
370
387
  assert_equal 10, json_response['data'].size
371
- assert_equal 'This is comment 9 on book 1.', json_response['data'][9]['body']
388
+ assert_equal 'This is comment 9 on book 1.', json_response['data'][9]['attributes']['body']
372
389
  end
373
390
 
374
391
  def test_pagination_related_resources_data_includes
@@ -377,7 +394,7 @@ class RequestTest < ActionDispatch::IntegrationTest
377
394
  get '/api/v2/books/1/book_comments?page[limit]=10&include=author,book'
378
395
  assert_equal 200, status
379
396
  assert_equal 10, json_response['data'].size
380
- assert_equal 'This is comment 9 on book 1.', json_response['data'][9]['body']
397
+ assert_equal 'This is comment 9 on book 1.', json_response['data'][9]['attributes']['body']
381
398
  end
382
399
 
383
400
  def test_flow_self
@@ -395,11 +412,11 @@ class RequestTest < ActionDispatch::IntegrationTest
395
412
  assert_equal 200, status
396
413
  post_1 = json_response['data'][0]
397
414
 
398
- get post_1['links']['author']['self']
415
+ get post_1['relationships']['author']['links']['self']
399
416
  assert_equal 200, status
400
417
  assert_hash_equals(json_response, {
401
418
  'links' => {
402
- 'self' => 'http://www.example.com/posts/1/links/author',
419
+ 'self' => 'http://www.example.com/posts/1/relationships/author',
403
420
  'related' => 'http://www.example.com/posts/1/author'
404
421
  },
405
422
  'data' => {type: 'people', id: '1'}
@@ -411,12 +428,12 @@ class RequestTest < ActionDispatch::IntegrationTest
411
428
  assert_equal 200, status
412
429
  post_1 = json_response['data'][0]
413
430
 
414
- get post_1['links']['tags']['self']
431
+ get post_1['relationships']['tags']['links']['self']
415
432
  assert_equal 200, status
416
433
  assert_hash_equals(json_response,
417
434
  {
418
435
  'links' => {
419
- 'self' => 'http://www.example.com/posts/1/links/tags',
436
+ 'self' => 'http://www.example.com/posts/1/relationships/tags',
420
437
  'related' => 'http://www.example.com/posts/1/tags'
421
438
  },
422
439
  'data' => [
@@ -432,18 +449,18 @@ class RequestTest < ActionDispatch::IntegrationTest
432
449
  assert_equal 200, status
433
450
  post_1 = json_response['data'][4]
434
451
 
435
- post post_1['links']['tags']['self'],
452
+ post post_1['relationships']['tags']['links']['self'],
436
453
  {'data' => [{'type' => 'tags', 'id' => '10'}]}.to_json,
437
454
  "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
438
455
 
439
456
  assert_equal 204, status
440
457
 
441
- get post_1['links']['tags']['self']
458
+ get post_1['relationships']['tags']['links']['self']
442
459
  assert_equal 200, status
443
460
  assert_hash_equals(json_response,
444
461
  {
445
462
  'links' => {
446
- 'self' => 'http://www.example.com/posts/5/links/tags',
463
+ 'self' => 'http://www.example.com/posts/5/relationships/tags',
447
464
  'related' => 'http://www.example.com/posts/5/tags'
448
465
  },
449
466
  'data' => [
@@ -499,7 +516,9 @@ class RequestTest < ActionDispatch::IntegrationTest
499
516
  post '/api/v6/purchase-orders',
500
517
  {
501
518
  'data' => {
502
- 'delivery-name' => 'ASDFG Corp',
519
+ 'attributes' => {
520
+ 'delivery-name' => 'ASDFG Corp'
521
+ },
503
522
  'type' => 'purchase-orders'
504
523
  }
505
524
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -513,7 +532,9 @@ class RequestTest < ActionDispatch::IntegrationTest
513
532
  post '/api/v6/purchase-orders',
514
533
  {
515
534
  'data' => {
516
- 'delivery_name' => 'ASDFG Corp',
535
+ 'attributes' => {
536
+ 'delivery_name' => 'ASDFG Corp'
537
+ },
517
538
  'type' => 'purchase_orders'
518
539
  }
519
540
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -527,7 +548,9 @@ class RequestTest < ActionDispatch::IntegrationTest
527
548
  post '/api/v7/purchase_orders',
528
549
  {
529
550
  'data' => {
530
- 'delivery-name' => 'ASDFG Corp',
551
+ 'attributes' => {
552
+ 'delivery-name' => 'ASDFG Corp'
553
+ },
531
554
  'type' => 'purchase-orders'
532
555
  }
533
556
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -541,7 +564,9 @@ class RequestTest < ActionDispatch::IntegrationTest
541
564
  post '/api/v6/purchase-orders',
542
565
  {
543
566
  'data' => {
544
- 'delivery_name' => 'ASDFG Corp',
567
+ 'attributes' => {
568
+ 'delivery_name' => 'ASDFG Corp'
569
+ },
545
570
  'type' => 'purchase-orders'
546
571
  }
547
572
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -556,7 +581,9 @@ class RequestTest < ActionDispatch::IntegrationTest
556
581
  {
557
582
  'data' => {
558
583
  'id' => '1',
559
- 'delivery-name' => 'ASDFG Corp',
584
+ 'attributes' => {
585
+ 'delivery-name' => 'ASDFG Corp'
586
+ },
560
587
  'type' => 'purchase-orders'
561
588
  }
562
589
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -572,10 +599,12 @@ class RequestTest < ActionDispatch::IntegrationTest
572
599
  'data' => {
573
600
  'id' => '1',
574
601
  'type' => 'line-items',
575
- 'item-cost' => '23.57',
576
- 'links' => {
602
+ 'attributes' => {
603
+ 'item-cost' => '23.57'
604
+ },
605
+ 'relationships' => {
577
606
  'purchase-order' => {
578
- 'linkage' => {'type' => 'purchase-orders', 'id' => '2'}
607
+ 'data' => {'type' => 'purchase-orders', 'id' => '2'}
579
608
  }
580
609
  }
581
610
  }
@@ -592,15 +621,15 @@ class RequestTest < ActionDispatch::IntegrationTest
592
621
  'data' => {
593
622
  'id' => '2',
594
623
  'type' => 'purchase-orders',
595
- 'links' => {
624
+ 'relationships' => {
596
625
  'line-items' => {
597
- 'linkage' => [
626
+ 'data' => [
598
627
  {'type' => 'line-items', 'id' => '3'},
599
628
  {'type' => 'line-items', 'id' => '4'}
600
629
  ]
601
630
  },
602
631
  'order-flags' => {
603
- 'linkage' => [
632
+ 'data' => [
604
633
  {'type' => 'order-flags', 'id' => '1'},
605
634
  {'type' => 'order-flags', 'id' => '2'}
606
635
  ]
@@ -611,4 +640,44 @@ class RequestTest < ActionDispatch::IntegrationTest
611
640
 
612
641
  assert_equal 200, status
613
642
  end
643
+
644
+ def test_post_has_many_link
645
+ JSONAPI.configuration.route_format = :dasherized_route
646
+ JSONAPI.configuration.json_key_format = :dasherized_key
647
+ post '/api/v6/purchase-orders/3/relationships/line-items',
648
+ {
649
+ 'data' => [
650
+ {'type' => 'line-items', 'id' => '3'},
651
+ {'type' => 'line-items', 'id' => '4'}
652
+ ]
653
+ }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
654
+
655
+ assert_equal 204, status
656
+ end
657
+
658
+ def test_patch_has_many_link
659
+ JSONAPI.configuration.route_format = :dasherized_route
660
+ JSONAPI.configuration.json_key_format = :dasherized_key
661
+ patch '/api/v6/purchase-orders/3/relationships/order-flags',
662
+ {
663
+ 'data' => [
664
+ {'type' => 'order-flags', 'id' => '1'},
665
+ {'type' => 'order-flags', 'id' => '2'}
666
+ ]
667
+ }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
668
+
669
+ assert_equal 204, status
670
+ end
671
+
672
+ def test_patch_has_one
673
+ JSONAPI.configuration.route_format = :dasherized_route
674
+ JSONAPI.configuration.json_key_format = :dasherized_key
675
+ patch '/api/v6/line-items/5/relationships/purchase-order',
676
+ {
677
+ 'data' => {'type' => 'purchase-orders', 'id' => '3'}
678
+ }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
679
+
680
+ assert_equal 204, status
681
+ end
682
+
614
683
  end