jsonapi-resources 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -105,6 +105,18 @@ ActiveRecord::Schema.define do
105
105
  t.binary :photo, limit: 1.kilobyte
106
106
  t.boolean :cool
107
107
  end
108
+
109
+ create_table :books, force: true do |t|
110
+ t.string :title
111
+ t.string :isbn
112
+ end
113
+
114
+ create_table :book_comments, force: true do |t|
115
+ t.text :body
116
+ t.belongs_to :book, index: true
117
+ t.integer :author_id
118
+ t.timestamps
119
+ end
108
120
  end
109
121
 
110
122
  ### MODELS
@@ -202,6 +214,15 @@ class Breed
202
214
  end
203
215
  end
204
216
 
217
+ class Book < ActiveRecord::Base
218
+ has_many :book_comments
219
+ end
220
+
221
+ class BookComment < ActiveRecord::Base
222
+ belongs_to :author, class_name: 'Person', foreign_key: 'author_id'
223
+ belongs_to :book
224
+ end
225
+
205
226
  class BreedData
206
227
  def initialize
207
228
  @breeds = {}
@@ -234,16 +255,20 @@ $breed_data.add(Breed.new(3, 'to_delete'))
234
255
 
235
256
  ### CONTROLLERS
236
257
  class AuthorsController < JSONAPI::ResourceController
237
-
238
258
  end
239
259
 
240
260
  class PeopleController < JSONAPI::ResourceController
241
-
242
261
  end
243
262
 
244
263
  class PostsController < JSONAPI::ResourceController
245
264
  end
246
265
 
266
+ class CommentsController < JSONAPI::ResourceController
267
+ end
268
+
269
+ class SectionsController < JSONAPI::ResourceController
270
+ end
271
+
247
272
  class TagsController < JSONAPI::ResourceController
248
273
  end
249
274
 
@@ -308,6 +333,12 @@ module Api
308
333
 
309
334
  class PreferencesController < JSONAPI::ResourceController
310
335
  end
336
+
337
+ class BooksController < JSONAPI::ResourceController
338
+ end
339
+
340
+ class BookCommentsController < JSONAPI::ResourceController
341
+ end
311
342
  end
312
343
 
313
344
  module V3
@@ -324,6 +355,9 @@ module Api
324
355
 
325
356
  class IsoCurrenciesController < JSONAPI::ResourceController
326
357
  end
358
+
359
+ class BooksController < JSONAPI::ResourceController
360
+ end
327
361
  end
328
362
 
329
363
  module V5
@@ -362,7 +396,7 @@ class PersonResource < JSONAPI::Resource
362
396
  end
363
397
 
364
398
  class AuthorResource < JSONAPI::Resource
365
- attributes :id, :name, :email
399
+ attributes :name, :email
366
400
  model_name 'Person'
367
401
  has_many :posts
368
402
 
@@ -385,14 +419,14 @@ class AuthorResource < JSONAPI::Resource
385
419
  end
386
420
 
387
421
  class CommentResource < JSONAPI::Resource
388
- attributes :id, :body
422
+ attributes :body
389
423
  has_one :post
390
424
  has_one :author, class_name: 'Person'
391
425
  has_many :tags
392
426
  end
393
427
 
394
428
  class TagResource < JSONAPI::Resource
395
- attributes :id, :name
429
+ attributes :name
396
430
 
397
431
  has_many :posts
398
432
  # Not including the planets association so they don't get output
@@ -404,7 +438,6 @@ class SectionResource < JSONAPI::Resource
404
438
  end
405
439
 
406
440
  class PostResource < JSONAPI::Resource
407
- attribute :id
408
441
  attribute :title
409
442
  attribute :body
410
443
  attribute :subject
@@ -451,7 +484,7 @@ class PostResource < JSONAPI::Resource
451
484
  end
452
485
 
453
486
  filters :title, :author, :tags, :comments
454
- filter :id
487
+ filters :id, :ids
455
488
 
456
489
  def self.updateable_fields(context)
457
490
  super(context) - [:author, :subject]
@@ -469,6 +502,9 @@ class PostResource < JSONAPI::Resource
469
502
  case filter
470
503
  when :id
471
504
  verify_keys(values, context)
505
+ when :ids #coerce :ids to :id
506
+ verify_keys(values, context)
507
+ return :id, values
472
508
  end
473
509
  return filter, values
474
510
  end
@@ -490,19 +526,27 @@ end
490
526
 
491
527
  class IsoCurrencyResource < JSONAPI::Resource
492
528
  primary_key :code
493
- attributes :id, :name, :country_name, :minor_unit
529
+ attributes :name, :country_name, :minor_unit
494
530
 
495
531
  filter :country_name
496
532
  end
497
533
 
498
534
  class ExpenseEntryResource < JSONAPI::Resource
499
- attributes :id, :cost
535
+ attributes :cost
500
536
  attribute :transaction_date, format: :date
501
537
 
502
538
  has_one :iso_currency, foreign_key: 'currency_code'
503
539
  has_one :employee, class_name: 'Person'
504
540
  end
505
541
 
542
+ class EmployeeResource < JSONAPI::Resource
543
+ attributes :name, :email
544
+ model_name 'Person'
545
+ end
546
+
547
+ class FriendResource < JSONAPI::Resource
548
+ end
549
+
506
550
  class BreedResource < JSONAPI::Resource
507
551
  attribute :id, format_misspelled: :does_not_exist
508
552
  attribute :name, format: :title
@@ -528,7 +572,6 @@ class BreedResource < JSONAPI::Resource
528
572
  end
529
573
 
530
574
  class PlanetResource < JSONAPI::Resource
531
- attribute :id
532
575
  attribute :name
533
576
  attribute :description
534
577
 
@@ -539,18 +582,17 @@ class PlanetResource < JSONAPI::Resource
539
582
  end
540
583
 
541
584
  class PropertyResource < JSONAPI::Resource
542
- attributes :id, :name
585
+ attributes :name
543
586
 
544
587
  has_many :planets
545
588
  end
546
589
 
547
590
  class PlanetTypeResource < JSONAPI::Resource
548
- attributes :id, :name
591
+ attributes :name
549
592
  has_many :planets
550
593
  end
551
594
 
552
595
  class MoonResource < JSONAPI::Resource
553
- attribute :id
554
596
  attribute :name
555
597
  attribute :description
556
598
 
@@ -558,11 +600,10 @@ class MoonResource < JSONAPI::Resource
558
600
  end
559
601
 
560
602
  class PreferencesResource < JSONAPI::Resource
561
- attribute :id
562
603
  attribute :advanced_mode
563
604
 
564
605
  has_one :author, foreign_key: :person_id
565
- has_many :friends
606
+ has_many :friends, class_name: 'Person'
566
607
 
567
608
  def self.find_by_key(key, options = {})
568
609
  new(Preferences.first)
@@ -570,7 +611,6 @@ class PreferencesResource < JSONAPI::Resource
570
611
  end
571
612
 
572
613
  class FactResource < JSONAPI::Resource
573
- attribute :id
574
614
  attribute :spouse_name
575
615
  attribute :bio
576
616
  attribute :quality_rating
@@ -585,7 +625,7 @@ end
585
625
  module Api
586
626
  module V1
587
627
  class WriterResource < JSONAPI::Resource
588
- attributes :id, :name, :email
628
+ attributes :name, :email
589
629
  model_name 'Person'
590
630
  has_many :posts
591
631
 
@@ -597,7 +637,6 @@ module Api
597
637
 
598
638
  class PostResource < JSONAPI::Resource
599
639
  # V1 no longer supports tags and now calls author 'writer'
600
- attribute :id
601
640
  attribute :title
602
641
  attribute :body
603
642
  attribute :subject
@@ -625,6 +664,8 @@ module Api
625
664
  PlanetTypeResource = PlanetTypeResource.dup
626
665
  MoonResource = MoonResource.dup
627
666
  PreferencesResource = PreferencesResource.dup
667
+ EmployeeResource = EmployeeResource.dup
668
+ FriendResource = FriendResource.dup
628
669
  end
629
670
  end
630
671
 
@@ -632,7 +673,21 @@ module Api
632
673
  module V2
633
674
  PreferencesResource = PreferencesResource.dup
634
675
  AuthorResource = AuthorResource.dup
676
+ PersonResource = PersonResource.dup
635
677
  PostResource = PostResource.dup
678
+
679
+ class BookResource < JSONAPI::Resource
680
+ attribute :title
681
+ attribute :isbn
682
+
683
+ has_many :book_comments
684
+ end
685
+
686
+ class BookCommentResource < JSONAPI::Resource
687
+ attributes :body
688
+ has_one :book
689
+ has_one :author, class_name: 'Person'
690
+ end
636
691
  end
637
692
  end
638
693
 
@@ -648,14 +703,20 @@ module Api
648
703
  PostResource = PostResource.dup
649
704
  ExpenseEntryResource = ExpenseEntryResource.dup
650
705
  IsoCurrencyResource = IsoCurrencyResource.dup
706
+
707
+ class BookResource < Api::V2::BookResource
708
+ paginator :paged
709
+ end
651
710
  end
652
711
  end
653
712
 
654
713
  module Api
655
714
  module V5
715
+ AuthorResource = AuthorResource.dup
656
716
  PostResource = PostResource.dup
657
717
  ExpenseEntryResource = ExpenseEntryResource.dup
658
718
  IsoCurrencyResource = IsoCurrencyResource.dup
719
+ EmployeeResource = EmployeeResource.dup
659
720
  end
660
721
  end
661
722
 
@@ -841,3 +902,14 @@ fact = Fact.create(spouse_name: 'Jane Author',
841
902
  photo: "abc",
842
903
  cool: false
843
904
  )
905
+
906
+ for book_num in 0..999
907
+ Book.create(title: "Book #{book_num}", isbn: "12345-#{book_num}-67890") do |book|
908
+ book.save
909
+ if book_num < 5
910
+ for comment_num in 0..50
911
+ book.book_comments.create(body: "This is comment #{comment_num} on book #{book_num}.", author_id: a.id, book_id: book.id)
912
+ end
913
+ end
914
+ end
915
+ end
@@ -3,56 +3,87 @@ require File.expand_path('../../../fixtures/active_record', __FILE__)
3
3
 
4
4
  class RequestTest < ActionDispatch::IntegrationTest
5
5
 
6
+ def setup
7
+ JSONAPI.configuration.json_key_format = :underscored_key
8
+ end
9
+
10
+ def after_teardown
11
+ Api::V2::BookResource.paginator :offset
12
+ JSONAPI.configuration.route_format = :underscored_route
13
+ end
14
+
6
15
  def test_get
7
16
  get '/posts'
8
17
  assert_equal 200, status
9
18
  end
10
19
 
20
+ def test_get_nested_has_one
21
+ get '/posts/1/author'
22
+ assert_equal 200, status
23
+ end
24
+
25
+ def test_get_nested_has_many
26
+ get '/posts/1/comments'
27
+ assert_equal 200, status
28
+ end
29
+
30
+ def test_get_nested_has_many_bad_param
31
+ get '/posts/1/comments?association=books'
32
+ assert_equal 200, status
33
+ end
34
+
11
35
  def test_get_underscored_key
12
36
  JSONAPI.configuration.json_key_format = :underscored_key
13
37
  get '/iso_currencies'
14
38
  assert_equal 200, status
15
- assert_equal 3, json_response['iso_currencies'].size
39
+ assert_equal 3, json_response['data'].size
16
40
  end
17
41
 
18
42
  def test_get_underscored_key_filtered
19
43
  JSONAPI.configuration.json_key_format = :underscored_key
20
- get '/iso_currencies?country_name=Canada'
44
+ get '/iso_currencies?filter[country_name]=Canada'
21
45
  assert_equal 200, status
22
- assert_equal 1, json_response['iso_currencies'].size
23
- assert_equal 'Canada', json_response['iso_currencies'][0]['country_name']
46
+ assert_equal 1, json_response['data'].size
47
+ assert_equal 'Canada', json_response['data'][0]['country_name']
24
48
  end
25
49
 
26
50
  def test_get_camelized_key_filtered
27
51
  JSONAPI.configuration.json_key_format = :camelized_key
28
- get '/iso_currencies?countryName=Canada'
52
+ get '/iso_currencies?filter[countryName]=Canada'
29
53
  assert_equal 200, status
30
- assert_equal 1, json_response['isoCurrencies'].size
31
- assert_equal 'Canada', json_response['isoCurrencies'][0]['countryName']
54
+ assert_equal 1, json_response['data'].size
55
+ assert_equal 'Canada', json_response['data'][0]['countryName']
32
56
  end
33
57
 
34
58
  def test_get_camelized_route_and_key_filtered
35
- get '/api/v4/isoCurrencies?countryName=Canada'
59
+ JSONAPI.configuration.json_key_format = :camelized_key
60
+ get '/api/v4/isoCurrencies?filter[countryName]=Canada'
36
61
  assert_equal 200, status
37
- assert_equal 1, json_response['isoCurrencies'].size
38
- assert_equal 'Canada', json_response['isoCurrencies'][0]['countryName']
62
+ assert_equal 1, json_response['data'].size
63
+ assert_equal 'Canada', json_response['data'][0]['countryName']
39
64
  end
40
65
 
41
66
  def test_get_camelized_route_and_links
42
67
  JSONAPI.configuration.json_key_format = :camelized_key
68
+ JSONAPI.configuration.route_format = :camelized_route
43
69
  get '/api/v4/expenseEntries/1/links/isoCurrency'
44
70
  assert_equal 200, status
45
- assert_equal 'USD', json_response['isoCurrency']
71
+ assert_hash_equals({'data' => {
72
+ 'type' => 'isoCurrencies',
73
+ 'id' => 'USD',
74
+ 'self' => 'http://www.example.com/api/v4/expenseEntries/1/links/isoCurrency',
75
+ 'resource' => 'http://www.example.com/api/v4/expenseEntries/1/isoCurrency'}}, json_response)
46
76
  end
47
77
 
48
78
  def test_put_single_without_content_type
49
79
  put '/posts/3',
50
80
  {
51
- 'posts' => {
81
+ 'data' => {
82
+ 'type' => 'posts',
52
83
  'id' => '3',
53
84
  'title' => 'A great new Post',
54
85
  'links' => {
55
- 'tags' => [3, 4]
86
+ 'tags' => {type: 'tags', ids: [3, 4]}
56
87
  }
57
88
  }
58
89
  }.to_json, "CONTENT_TYPE" => "application/json"
@@ -63,11 +94,12 @@ class RequestTest < ActionDispatch::IntegrationTest
63
94
  def test_put_single
64
95
  put '/posts/3',
65
96
  {
66
- 'posts' => {
97
+ 'data' => {
98
+ 'type' => 'posts',
67
99
  'id' => '3',
68
100
  'title' => 'A great new Post',
69
101
  'links' => {
70
- 'tags' => [3, 4]
102
+ 'tags' => {type: 'tags', ids: [3, 4]}
71
103
  }
72
104
  }
73
105
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -92,11 +124,12 @@ class RequestTest < ActionDispatch::IntegrationTest
92
124
  def test_post_single
93
125
  post '/posts',
94
126
  {
95
- 'posts' => {
127
+ 'data' => {
128
+ 'type' => 'posts',
96
129
  'title' => 'A great new Post',
97
130
  'body' => 'JSONAPIResources is the greatest thing since unsliced bread.',
98
131
  'links' => {
99
- 'author' => '3'
132
+ 'author' => {type: 'people', id: '3'}
100
133
  }
101
134
  }
102
135
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -104,16 +137,16 @@ class RequestTest < ActionDispatch::IntegrationTest
104
137
  assert_equal 201, status
105
138
  end
106
139
 
107
- def test_create_association_without_content_type
140
+ def test_update_association_without_content_type
108
141
  ruby = Section.find_by(name: 'ruby')
109
- put '/posts/3/links/section', { 'sections' => ruby.id.to_s }.to_json
142
+ put '/posts/3/links/section', { 'sections' => {type: 'sections', id: ruby.id.to_s }}.to_json
110
143
 
111
144
  assert_equal 415, status
112
145
  end
113
146
 
114
- def test_create_association
147
+ def test_update_association_has_one
115
148
  ruby = Section.find_by(name: 'ruby')
116
- put '/posts/3/links/section', { 'sections' => ruby.id.to_s }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
149
+ put '/posts/3/links/section', { 'data' => {type: 'sections', id: ruby.id.to_s }}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
117
150
 
118
151
  assert_equal 204, status
119
152
  end
@@ -131,11 +164,12 @@ class RequestTest < ActionDispatch::IntegrationTest
131
164
  def test_put_content_type
132
165
  put '/posts/3',
133
166
  {
134
- 'posts' => {
167
+ 'data' => {
168
+ 'type' => 'posts',
135
169
  'id' => '3',
136
170
  'title' => 'A great new Post',
137
171
  'links' => {
138
- 'tags' => [3, 4]
172
+ 'tags' => {type: 'tags', ids: [3, 4]}
139
173
  }
140
174
  }
141
175
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -146,10 +180,11 @@ class RequestTest < ActionDispatch::IntegrationTest
146
180
  def test_post_correct_content_type
147
181
  post '/posts',
148
182
  {
149
- 'posts' => {
183
+ 'data' => {
184
+ 'type' => 'posts',
150
185
  'title' => 'A great new Post',
151
186
  'links' => {
152
- 'author' => '3'
187
+ 'author' => {type: 'people', id: '3'}
153
188
  }
154
189
  }
155
190
  }.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
@@ -167,4 +202,127 @@ class RequestTest < ActionDispatch::IntegrationTest
167
202
  delete '/posts/8,9'
168
203
  assert_equal 204, status
169
204
  end
205
+
206
+ def test_pagination_none
207
+ Api::V2::BookResource.paginator :none
208
+ get '/api/v2/books'
209
+ assert_equal 200, status
210
+ assert_equal 1000, json_response['data'].size
211
+ end
212
+
213
+ def test_pagination_offset_style
214
+ Api::V2::BookResource.paginator :offset
215
+ get '/api/v2/books'
216
+ assert_equal 200, status
217
+ assert_equal JSONAPI.configuration.default_page_size, json_response['data'].size
218
+ assert_equal 'Book 0', json_response['data'][0]['title']
219
+ end
220
+
221
+ def test_pagination_offset_style_offset
222
+ Api::V2::BookResource.paginator :offset
223
+ get '/api/v2/books?page[offset]=50'
224
+ assert_equal 200, status
225
+ assert_equal JSONAPI.configuration.default_page_size, json_response['data'].size
226
+ assert_equal 'Book 50', json_response['data'][0]['title']
227
+ end
228
+
229
+ def test_pagination_offset_style_offset_limit
230
+ Api::V2::BookResource.paginator :offset
231
+ get '/api/v2/books?page[offset]=50&page[limit]=20'
232
+ assert_equal 200, status
233
+ assert_equal 20, json_response['data'].size
234
+ assert_equal 'Book 50', json_response['data'][0]['title']
235
+ end
236
+
237
+ def test_pagination_offset_bad_param
238
+ Api::V2::BookResource.paginator :offset
239
+ get '/api/v2/books?page[irishsetter]=50&page[limit]=20'
240
+ assert_equal 400, status
241
+ end
242
+
243
+ def test_pagination_related_resources_link
244
+ Api::V2::BookResource.paginator :offset
245
+ get '/api/v2/books?page[limit]=2'
246
+ assert_equal 200, status
247
+ assert_equal 2, json_response['data'].size
248
+ assert_equal 'http://www.example.com/api/v2/books/1/book_comments',
249
+ json_response['data'][0]['links']['book_comments']['resource']
250
+ end
251
+
252
+ def test_pagination_related_resources_data
253
+ Api::V2::BookResource.paginator :offset
254
+ Api::V2::BookCommentResource.paginator :offset
255
+ get '/api/v2/books/1/book_comments?page[limit]=10'
256
+ assert_equal 200, status
257
+ assert_equal 10, json_response['data'].size
258
+ assert_equal 'This is comment 9 on book 0.', json_response['data'][9]['body']
259
+ end
260
+
261
+ def test_pagination_related_resources_data_includes
262
+ Api::V2::BookResource.paginator :offset
263
+ Api::V2::BookCommentResource.paginator :offset
264
+ get '/api/v2/books/1/book_comments?page[limit]=10&include=author,book'
265
+ assert_equal 200, status
266
+ assert_equal 10, json_response['data'].size
267
+ assert_equal 'This is comment 9 on book 0.', json_response['data'][9]['body']
268
+ end
269
+
270
+ def test_flow_self
271
+ get '/posts'
272
+ assert_equal 200, status
273
+ post_1 = json_response['data'][0]
274
+
275
+ get post_1['links']['self']
276
+ assert_equal 200, status
277
+ assert_hash_equals post_1, json_response['data']
278
+ end
279
+
280
+ def test_flow_link_has_one_self_link
281
+ get '/posts'
282
+ assert_equal 200, status
283
+ post_1 = json_response['data'][0]
284
+
285
+ get post_1['links']['author']['self']
286
+ assert_equal 200, status
287
+ assert_hash_equals(json_response, {'data' => post_1['links']['author']})
288
+ end
289
+
290
+ def test_flow_link_has_many_self_link
291
+ get '/posts'
292
+ assert_equal 200, status
293
+ post_1 = json_response['data'][0]
294
+
295
+ get post_1['links']['tags']['self']
296
+ assert_equal 200, status
297
+ assert_hash_equals(json_response,
298
+ {'data' => {
299
+ 'self' => 'http://www.example.com/posts/1/links/tags',
300
+ 'resource' => 'http://www.example.com/posts/1/tags',
301
+ 'type' => 'tags', 'ids'=>['1', '2', '3']
302
+ }
303
+ })
304
+ end
305
+
306
+ def test_flow_link_has_many_self_link_put
307
+ get '/posts'
308
+ assert_equal 200, status
309
+ post_1 = json_response['data'][0]
310
+
311
+ post post_1['links']['tags']['self'],
312
+ {'data' => {'type' => 'tags', 'ids' => ['5']}}.to_json,
313
+ "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
314
+
315
+ assert_equal 204, status
316
+
317
+ get post_1['links']['tags']['self']
318
+ assert_equal 200, status
319
+ assert_hash_equals(json_response,
320
+ {'data' => {
321
+ 'self' => 'http://www.example.com/posts/1/links/tags',
322
+ 'resource' => 'http://www.example.com/posts/1/tags',
323
+ 'type' => 'tags', 'ids'=>['1', '2', '3', '5']
324
+ }
325
+ })
326
+ end
327
+
170
328
  end