jsonapi-resources 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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