json_api_client 1.23.0 → 1.24.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -20
  3. data/README.md +723 -705
  4. data/Rakefile +32 -32
  5. data/lib/json_api_client/associations/base_association.rb +33 -33
  6. data/lib/json_api_client/associations/belongs_to.rb +31 -31
  7. data/lib/json_api_client/associations/has_many.rb +7 -7
  8. data/lib/json_api_client/associations/has_one.rb +16 -16
  9. data/lib/json_api_client/associations.rb +7 -7
  10. data/lib/json_api_client/connection.rb +41 -41
  11. data/lib/json_api_client/error_collector.rb +91 -91
  12. data/lib/json_api_client/errors.rb +125 -125
  13. data/lib/json_api_client/formatter.rb +145 -145
  14. data/lib/json_api_client/helpers/associatable.rb +88 -88
  15. data/lib/json_api_client/helpers/callbacks.rb +27 -27
  16. data/lib/json_api_client/helpers/dirty.rb +75 -75
  17. data/lib/json_api_client/helpers/dynamic_attributes.rb +78 -78
  18. data/lib/json_api_client/helpers/uri.rb +9 -9
  19. data/lib/json_api_client/helpers.rb +9 -9
  20. data/lib/json_api_client/implementation.rb +11 -11
  21. data/lib/json_api_client/included_data.rb +58 -58
  22. data/lib/json_api_client/linking/links.rb +21 -21
  23. data/lib/json_api_client/linking/top_level_links.rb +39 -39
  24. data/lib/json_api_client/linking.rb +5 -5
  25. data/lib/json_api_client/meta_data.rb +19 -19
  26. data/lib/json_api_client/middleware/json_request.rb +26 -26
  27. data/lib/json_api_client/middleware/status.rb +67 -67
  28. data/lib/json_api_client/middleware.rb +6 -6
  29. data/lib/json_api_client/paginating/nested_param_paginator.rb +140 -140
  30. data/lib/json_api_client/paginating/paginator.rb +89 -89
  31. data/lib/json_api_client/paginating.rb +6 -6
  32. data/lib/json_api_client/parsers/parser.rb +102 -102
  33. data/lib/json_api_client/parsers.rb +4 -4
  34. data/lib/json_api_client/query/builder.rb +239 -239
  35. data/lib/json_api_client/query/requestor.rb +73 -73
  36. data/lib/json_api_client/query.rb +5 -5
  37. data/lib/json_api_client/relationships/relations.rb +55 -55
  38. data/lib/json_api_client/relationships/top_level_relations.rb +30 -30
  39. data/lib/json_api_client/relationships.rb +5 -5
  40. data/lib/json_api_client/request_params.rb +57 -57
  41. data/lib/json_api_client/resource.rb +671 -671
  42. data/lib/json_api_client/result_set.rb +25 -25
  43. data/lib/json_api_client/schema.rb +154 -154
  44. data/lib/json_api_client/utils.rb +53 -53
  45. data/lib/json_api_client/version.rb +3 -3
  46. data/lib/json_api_client.rb +30 -30
  47. metadata +55 -30
data/README.md CHANGED
@@ -1,705 +1,723 @@
1
- # JsonApiClient [![Build Status](https://travis-ci.org/JsonApiClient/json_api_client.png?branch=master)](https://travis-ci.org/JsonApiClient/json_api_client) [![Code Climate](https://codeclimate.com/github/JsonApiClient/json_api_client.png)](https://codeclimate.com/github/JsonApiClient/json_api_client) [![Code Coverage](https://codeclimate.com/github/JsonApiClient/json_api_client/coverage.png)](https://codeclimate.com/github/JsonApiClient/json_api_client)
2
-
3
- This gem is meant to help you build an API client for interacting with REST APIs as laid out by [http://jsonapi.org](http://jsonapi.org). It attempts to give you a query building framework that is easy to understand (it is similar to ActiveRecord scopes).
4
-
5
- *Note: master is currently tracking the 1.0.0 specification. If you're looking for the older code, see [0.x branch](https://github.com/JsonApiClient/json_api_client/tree/0.x)*
6
-
7
- ## Usage
8
-
9
- You will want to create your own resource classes that inherit from `JsonApiClient::Resource` similar to how you would create an `ActiveRecord` class. You may also want to create your own abstract base class to share common behavior. Additionally, you will probably want to namespace your models. Namespacing your model will not affect the url routing to that resource.
10
-
11
- ```ruby
12
- module MyApi
13
- # this is an "abstract" base class that
14
- class Base < JsonApiClient::Resource
15
- # set the api base url in an abstract base class
16
- self.site = "http://example.com/"
17
- end
18
-
19
- class Article < Base
20
- end
21
-
22
- class Comment < Base
23
- end
24
-
25
- class Person < Base
26
- end
27
- end
28
- ```
29
-
30
- By convention, we guess the resource route from the class name. In the above example, `Article`'s path is "http://example.com/articles" and `Person`'s path would be "http://example.com/people".
31
-
32
- Some basic example usage:
33
-
34
- ```ruby
35
- MyApi::Article.all
36
- MyApi::Article.where(author_id: 1).find(2)
37
- MyApi::Article.where(author_id: 1).all
38
-
39
- MyApi::Person.where(name: "foo").order(created_at: :desc).includes(:preferences, :cars).all
40
-
41
- u = MyApi::Person.new(first_name: "bar", last_name: "foo")
42
- u.new_record?
43
- # => true
44
- u.save
45
-
46
- u.new_record?
47
- # => false
48
-
49
- u = MyApi::Person.find(1).first
50
- u.update_attributes(
51
- a: "b",
52
- c: "d"
53
- )
54
-
55
- u.persisted?
56
- # => true
57
-
58
- u.destroy
59
-
60
- u.destroyed?
61
- # => true
62
- u.persisted?
63
- # => false
64
-
65
- u = MyApi::Person.create(
66
- a: "b",
67
- c: "d"
68
- )
69
- ```
70
-
71
- All class level finders/creators should return a `JsonApiClient::ResultSet` which behaves like an Array and contains extra data about the api response.
72
-
73
-
74
- ## Handling Validation Errors
75
-
76
- [See specification](http://jsonapi.org/format/#errors)
77
-
78
- Out of the box, `json_api_client` handles server side validation only.
79
-
80
- ```ruby
81
- User.create(name: "Bob", email_address: "invalid email")
82
- # => false
83
-
84
- user = User.new(name: "Bob", email_address: "invalid email")
85
- user.save
86
- # => false
87
-
88
- # returns an error collector which is array-like
89
- user.errors
90
- # => ["Email address is invalid"]
91
-
92
- # get all error titles
93
- user.errors.full_messages
94
- # => ["Email address is invalid"]
95
-
96
- # get errors for a specific parameter
97
- user.errors[:email_address]
98
- # => ["Email address is invalid"]
99
-
100
- user = User.find(1)
101
- user.update_attributes(email_address: "invalid email")
102
- # => false
103
-
104
- user.errors
105
- # => ["Email address is invalid"]
106
-
107
- user.email_address
108
- # => "invalid email"
109
- ```
110
-
111
- For now we are assuming that error sources are all parameters.
112
-
113
- If you want to add client side validation, I suggest creating a form model class that uses ActiveModel's validations.
114
-
115
- ## Meta information
116
-
117
- [See specification](http://jsonapi.org/format/#document-structure-meta)
118
-
119
- If the response has a top level meta data section, we can access it via the `meta` accessor on `ResultSet`.
120
-
121
- ```ruby
122
- # Example response:
123
- {
124
- "meta": {
125
- "copyright": "Copyright 2015 Example Corp.",
126
- "authors": [
127
- "Yehuda Katz",
128
- "Steve Klabnik",
129
- "Dan Gebhardt"
130
- ]
131
- },
132
- "data": {
133
- // ...
134
- }
135
- }
136
- articles = Articles.all
137
-
138
- articles.meta.copyright
139
- # => "Copyright 2015 Example Corp."
140
-
141
- articles.meta.authors
142
- # => ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt"]
143
- ```
144
-
145
- ## Top-level Links
146
-
147
- [See specification](http://jsonapi.org/format/#document-structure-top-level-links)
148
-
149
- If the resource returns top level links, we can access them via the `links` accessor on `ResultSet`.
150
-
151
- ```ruby
152
- articles = Articles.find(1)
153
- articles.links.related
154
- ```
155
-
156
- ## Nested Resources
157
-
158
- You can force nested resource paths for your models by using a `belongs_to` association.
159
-
160
- **Note: Using belongs_to is only necessary for setting a nested path unless you provide `shallow_path: true` option.**
161
-
162
- ```ruby
163
- module MyApi
164
- class Account < JsonApiClient::Resource
165
- belongs_to :user
166
- end
167
-
168
- class Customer < JsonApiClient::Resource
169
- belongs_to :user, shallow_path: true
170
- end
171
- end
172
-
173
- # try to find without the nested parameter
174
- MyApi::Account.find(1)
175
- # => raises ArgumentError
176
-
177
- # makes request to /users/2/accounts/1
178
- MyApi::Account.where(user_id: 2).find(1)
179
- # => returns ResultSet
180
-
181
- # makes request to /customers/1
182
- MyApi::Customer.find(1)
183
- # => returns ResultSet
184
-
185
- # makes request to /users/2/customers/1
186
- MyApi::Customer.where(user_id: 2).find(1)
187
- # => returns ResultSet
188
- ```
189
-
190
- you can also override param name for `belongs_to` association
191
-
192
- ```ruby
193
- module MyApi
194
- class Account < JsonApiClient::Resource
195
- belongs_to :user, param: :customer_id
196
- end
197
- end
198
-
199
- # makes request to /users/2/accounts/1
200
- MyApi::Account.where(customer_id: 2).find(1)
201
- # => returns ResultSet
202
- ```
203
-
204
- ## Custom Methods
205
-
206
- You can create custom methods on both collections (class method) and members (instance methods).
207
-
208
- ```ruby
209
- module MyApi
210
- class User < JsonApiClient::Resource
211
- # GET /users/search
212
- custom_endpoint :search, on: :collection, request_method: :get
213
-
214
- # PUT /users/:id/verify
215
- custom_endpoint :verify, on: :member, request_method: :put
216
- end
217
- end
218
-
219
- # makes GET request to /users/search?name=Jeff
220
- MyApi::User.search(name: 'Jeff')
221
- # => <ResultSet of MyApi::User instances>
222
-
223
- user = MyApi::User.find(1)
224
- # makes PUT request to /users/1/verify?foo=bar
225
- user.verify(foo: 'bar')
226
- ```
227
-
228
- ## Fetching Includes
229
-
230
- [See specification](http://jsonapi.org/format/#fetching-includes)
231
-
232
- If the response returns a [compound document](http://jsonapi.org/format/#document-compound-documents), then we should be able to get the related resources.
233
-
234
- ```ruby
235
- # makes request to /articles/1?include=author,comments.author
236
- results = Article.includes(:author, :comments => :author).find(1)
237
-
238
- # should not have to make additional requests to the server
239
- authors = results.map(&:author)
240
-
241
- # makes POST request to /articles?include=author,comments.author
242
- article = Article.new(title: 'New one').request_includes(:author, :comments => :author)
243
- article.save
244
-
245
- # makes PATCH request to /articles/1?include=author,comments.author
246
- article = Article.find(1)
247
- article.title = 'Changed'
248
- article.request_includes(:author, :comments => :author)
249
- article.save
250
-
251
- # request includes will be cleared if response is successful
252
- # to avoid this `keep_request_params` class attribute can be used
253
- Article.keep_request_params = true
254
-
255
- # to clear request_includes use
256
- article.reset_request_includes!
257
- ```
258
-
259
- ## Sparse Fieldsets
260
-
261
- [See specification](http://jsonapi.org/format/#fetching-sparse-fieldsets)
262
-
263
- ```ruby
264
- # makes request to /articles?fields[articles]=title,body
265
- article = Article.select("title", "body").first
266
-
267
- # should have fetched the requested fields
268
- article.title
269
- # => "Rails is Omakase"
270
-
271
- # should not have returned the created_at
272
- article.created_at
273
- # => raise NoMethodError
274
-
275
- # or you can use fieldsets from multiple resources
276
- # makes request to /articles?fields[articles]=title,body&fields[comments]=tag
277
- article = Article.select("title", "body",{comments: 'tag'}).first
278
-
279
- # makes POST request to /articles?fields[articles]=title,body&fields[comments]=tag
280
- article = Article.new(title: 'New one').request_select(:title, :body, comments: 'tag')
281
- article.save
282
-
283
- # makes PATCH request to /articles/1?fields[articles]=title,body&fields[comments]=tag
284
- article = Article.find(1)
285
- article.title = 'Changed'
286
- article.request_select(:title, :body, comments: 'tag')
287
- article.save
288
-
289
- # request fields will be cleared if response is successful
290
- # to avoid this `keep_request_params` class attribute can be used
291
- Article.keep_request_params = true
292
-
293
- # to clear request fields use
294
- article.reset_request_select!(:comments) # to clear for comments
295
- article.reset_request_select! # to clear for all fields
296
- ```
297
-
298
- ## Sorting
299
-
300
- [See specification](http://jsonapi.org/format/#fetching-sorting)
301
-
302
- ```ruby
303
- # makes request to /people?sort=age
304
- youngest = Person.order(:age).all
305
-
306
- # also makes request to /people?sort=age
307
- youngest = Person.order(age: :asc).all
308
-
309
- # makes request to /people?sort=-age
310
- oldest = Person.order(age: :desc).all
311
- ```
312
-
313
- ## Paginating
314
-
315
- [See specification](http://jsonapi.org/format/#fetching-pagination)
316
-
317
- ### Requesting
318
-
319
- ```ruby
320
- # makes request to /articles?page=2&per_page=30
321
- articles = Article.page(2).per(30).to_a
322
-
323
- # also makes request to /articles?page=2&per_page=30
324
- articles = Article.paginate(page: 2, per_page: 30).to_a
325
-
326
- # keep in mind that page number can be nil - in that case default number will be applied
327
- # also makes request to /articles?page=1&per_page=30
328
- articles = Article.paginate(page: nil, per_page: 30).to_a
329
- ```
330
-
331
- *Note: The mapping of pagination parameters is done by the `query_builder` which is [customizable](#custom-paginator).*
332
-
333
- ### Browsing
334
-
335
- If the response contains additional pagination links, you can also get at those:
336
-
337
- ```ruby
338
- articles = Article.paginate(page: 2, per_page: 30).to_a
339
- articles.pages.next
340
- articles.pages.last
341
- ```
342
-
343
- ### Library compatibility
344
-
345
- A `JsonApiClient::ResultSet` object should be paginatable with both `kaminari` and `will_paginate`.
346
-
347
- ## Filtering
348
-
349
- [See specifiation](http://jsonapi.org/format/#fetching-filtering)
350
-
351
- ```ruby
352
- # makes request to /people?filter[name]=Jeff
353
- Person.where(name: 'Jeff').all
354
- ```
355
-
356
- ## Schema
357
-
358
- You can define schema within your client model. You can define basic types and set default values if you wish. If you declare a basic type, we will try to cast any input to be that type.
359
-
360
- The added benefit of declaring your schema is that you can access fields before data is set (otherwise, you'll get a `NoMethodError`).
361
-
362
- **Note: This is completely optional. This will set default values and handle typecasting.**
363
-
364
- ### Example
365
-
366
- ```ruby
367
- class User < JsonApiClient::Resource
368
- property :name, type: :string
369
- property :is_admin, type: :boolean, default: false
370
- property :points_accrued, type: :int, default: 0
371
- property :averge_points_per_day, type: :float
372
- end
373
-
374
- # default values
375
- u = User.new
376
-
377
- u.name
378
- # => nil
379
-
380
- u.is_admin
381
- # => false
382
-
383
- u.points_accrued
384
- # => 0
385
-
386
- # casting
387
- u.average_points_per_day = "0.3"
388
- u.average_points_per_day
389
- # => 0.3
390
- ```
391
-
392
- ### Types
393
-
394
- The basic types that we allow are:
395
-
396
- * `:int` or `:integer`
397
- * `:float`
398
- * `:string`
399
- * `:time` - *Note: Include the time zone in the string if it's different than local time.
400
- * `:boolean` - *Note: we will cast the string version of "true" and "false" to their respective values*
401
-
402
- Also, we consider `nil` to be an acceptable value and will not cast the value.
403
-
404
- Note : Do not map the primary key as int.
405
-
406
- ## Customizing
407
-
408
- ### Paths
409
-
410
- You can customize this path by changing your resource's `table_name`:
411
-
412
- ```ruby
413
- module MyApi
414
- class SomeResource < Base
415
- def self.table_name
416
- "foobar"
417
- end
418
- end
419
- end
420
-
421
- # requests http://example.com/foobar
422
- MyApi::SomeResource.all
423
- ```
424
-
425
- ### Custom headers
426
-
427
- You can inject custom headers on resource request by wrapping your code into block:
428
- ```ruby
429
- MyApi::SomeResource.with_headers(x_access_token: 'secure_token_here') do
430
- MyApi::SomeResource.find(1)
431
- end
432
- ```
433
-
434
- ### Connections
435
-
436
- You can configure your API client to use a custom connection that implementes the `run` instance method. It should return data that your parser can handle. The default connection class wraps Faraday and lets you add middleware.
437
-
438
- ```ruby
439
- class NullConnection
440
- def initialize(*args)
441
- end
442
-
443
- def run(request_method, path, params: nil, headers: {}, body: nil)
444
- end
445
-
446
- def use(*args); end
447
- end
448
-
449
- class CustomConnectionResource < TestResource
450
- self.connection_class = NullConnection
451
- end
452
- ```
453
-
454
- #### Connection Options
455
-
456
- You can configure your connection using Faraday middleware. In general, you'll want
457
- to do this in a base model that all your resources inherit from:
458
-
459
- ```ruby
460
- MyApi::Base.connection do |connection|
461
- # set OAuth2 headers
462
- connection.use FaradayMiddleware::OAuth2, 'MYTOKEN'
463
-
464
- # log responses
465
- connection.use Faraday::Response::Logger
466
-
467
- connection.use MyCustomMiddleware
468
- end
469
-
470
- module MyApi
471
- class User < Base
472
- # will use the customized connection
473
- end
474
- end
475
- ```
476
-
477
- ##### Server errors handling
478
-
479
- Non-success API response will cause the specific `JsonApiClient::Errors::SomeException` raised, depends on responded HTTP status.
480
- Please refer to [JsonApiClient::Middleware::Status#handle_status](https://github.com/JsonApiClient/json_api_client/blob/master/lib/json_api_client/middleware/status.rb)
481
- method for concrete status-to-exception mapping used out of the box.
482
-
483
- JsonApiClient will try determine is failed API response JsonApi-compatible, if so - JsonApi error messages will be parsed from response body, and tracked as a part of particular exception message. In additional, `JsonApiClient::Errors::ServerError` exception will keep the actual HTTP status and message within its message.
484
-
485
- ##### Custom status handler
486
-
487
- You can change handling of response status using `connection_options`. For example you can override 400 status handling.
488
- By default it raises `JsonApiClient::Errors::ClientError` but you can skip exception if you want to process errors from the server.
489
- You need to provide a `proc` which should call `throw(:handled)` default handler for this status should be skipped.
490
- ```ruby
491
- class ApiBadRequestHandler
492
- def self.call(_env)
493
- # do not raise exception
494
- end
495
- end
496
-
497
- class CustomUnauthorizedError < StandardError
498
- attr_reader :env
499
-
500
- def initialize(env)
501
- @env = env
502
- super('not authorized')
503
- end
504
- end
505
-
506
- MyApi::Base.connection_options[:status_handlers] = {
507
- 400 => ApiBadRequestHandler,
508
- 401 => ->(env) { raise CustomUnauthorizedError, env }
509
- }
510
-
511
- module MyApi
512
- class User < Base
513
- # will use the customized status_handlers
514
- end
515
- end
516
-
517
- user = MyApi::User.create(name: 'foo')
518
- # server responds with { errors: [ { detail: 'bad request' } ] }
519
- user.errors.messages # { base: ['bad request'] }
520
- # on 401 it will raise CustomUnauthorizedError instead of JsonApiClient::Errors::NotAuthorized
521
- ```
522
-
523
- ##### Specifying an HTTP Proxy
524
-
525
- All resources have a class method ```connection_options``` used to pass options to the JsonApiClient::Connection initializer.
526
-
527
- ```ruby
528
- MyApi::Base.connection_options[:proxy] = 'http://proxy.example.com'
529
- MyApi::Base.connection do |connection|
530
- # ...
531
- end
532
-
533
- module MyApi
534
- class User < Base
535
- # will use the customized connection with proxy
536
- end
537
- end
538
- ```
539
-
540
- ### Custom Parser
541
-
542
- You can configure your API client to use a custom parser that implements the `parse` class method. It should return a `JsonApiClient::ResultSet` instance. You can use it by setting the parser attribute on your model:
543
-
544
- ```ruby
545
- class MyCustomParser
546
- def self.parse(klass, response)
547
- # …
548
- # returns some ResultSet object
549
- end
550
- end
551
-
552
- class MyApi::Base < JsonApiClient::Resource
553
- self.parser = MyCustomParser
554
- end
555
- ```
556
-
557
- ### Custom Query Builder
558
-
559
- You can customize how the scope builder methods map to request parameters.
560
-
561
- ```ruby
562
- class MyQueryBuilder
563
- def initialize(klass); end
564
-
565
- def where(conditions = {})
566
- end
567
-
568
- # … add order, includes, paginate, page, first, build
569
- end
570
-
571
- class MyApi::Base < JsonApiClient::Resource
572
- self.query_builder = MyQueryBuilder
573
- end
574
- ```
575
-
576
- ### Custom Paginator
577
-
578
- You can customize how your resources find pagination information from the response.
579
-
580
- If the [existing paginator](https://github.com/JsonApiClient/json_api_client/blob/master/lib/json_api_client/paginating/paginator.rb) fits your requirements but you don't use the default `page` and `per_page` params for pagination, you can customise the param keys as follows:
581
-
582
- ```ruby
583
- JsonApiClient::Paginating::Paginator.page_param = "number"
584
- JsonApiClient::Paginating::Paginator.per_page_param = "size"
585
- ```
586
-
587
- Please note that this is a global configuration, so library authors should create a custom paginator that inherits `JsonApiClient::Paginating::Paginator` and configure the custom paginator to avoid modifying global config.
588
-
589
- If the [existing paginator](https://github.com/JsonApiClient/json_api_client/blob/master/lib/json_api_client/paginating/paginator.rb) does not fit your needs, you can create a custom paginator:
590
-
591
- ```ruby
592
- class MyPaginator
593
- def initialize(result_set, data); end
594
- # implement current_page, total_entries, etc
595
- end
596
-
597
- class MyApi::Base < JsonApiClient::Resource
598
- self.paginator = MyPaginator
599
- end
600
- ```
601
-
602
- ### NestedParamPaginator
603
-
604
- The default `JsonApiClient::Paginating::Paginator` is not strict about how it handles the param keys ([#347](https://github.com/JsonApiClient/json_api_client/issues/347)). There is a second paginator that more rigorously adheres to the JSON:API pagination recommendation style of `page[page]=1&page[per_page]=10`.
605
-
606
- If this second style suits your needs better, it is available as a class override:
607
-
608
- ```ruby
609
- class Order < JsonApiClient::Resource
610
- self.paginator = JsonApiClient::Paginating::NestedParamPaginator
611
- end
612
- ```
613
-
614
- You can also extend `NestedParamPaginator` in your custom paginators or assign the `page_param` or `per_page_param` as with the default version above.
615
-
616
- ### Custom type
617
-
618
- If your model must be named differently from classified type of resource you can easily customize it.
619
- It will work both for defined and not defined relationships
620
-
621
- ```ruby
622
- class MyApi::Base < JsonApiClient::Resource
623
- resolve_custom_type 'document--files', 'File'
624
- end
625
-
626
- class MyApi::File < MyApi::Base
627
- def self.resource_name
628
- 'document--files'
629
- end
630
- end
631
- ```
632
-
633
- ### Type Casting
634
-
635
- You can define your own types and its casting mechanism for schema.
636
-
637
- ```ruby
638
- require 'money'
639
- class MyMoneyCaster
640
- def self.cast(value, default)
641
- begin
642
- Money.new(value, "USD")
643
- rescue ArgumentError
644
- default
645
- end
646
- end
647
- end
648
-
649
- JsonApiClient::Schema.register money: MyMoneyCaster
650
-
651
- ```
652
- and finally
653
-
654
- ```ruby
655
- class Order < JsonApiClient::Resource
656
- property :total_amount, type: :money
657
- end
658
-
659
- ```
660
-
661
- ### Safe singular resource fetching
662
-
663
- That is a bit curios, but `json_api_client` returns an array from `.find` method, always.
664
- The history of this fact was discussed [here](https://github.com/JsonApiClient/json_api_client/issues/75)
665
-
666
- So, when we searching for a single resource by primary key, we typically write the things like
667
-
668
- ```ruby
669
- admin = User.find(id).first
670
- ```
671
-
672
- The next thing which we need to notice - `json_api_client` will just interpolate the incoming `.find` param to the end of API URL, just like that:
673
-
674
- > http://somehost/api/v1/users/{id}
675
-
676
- What will happen if we pass the blank id (nil or empty string) to the `.find` method then?.. Yeah, `json_api_client` will try to call the INDEX API endpoint instead of SHOW one:
677
-
678
- > http://somehost/api/v1/users/
679
-
680
- Lets sum all together - in case if `id` comes blank (from CGI for instance), we can silently receive the `admin` variable equal to some existing resource, with all the consequences.
681
-
682
- Even worse, `admin` variable can equal to *random* resource, depends on ordering applied by INDEX endpoint.
683
-
684
- If you prefer to get `JsonApiClient::Errors::NotFound` raised, please define in your base Resource class:
685
-
686
- ```ruby
687
- class Resource < JsonApiClient::Resource
688
- self.raise_on_blank_find_param = true
689
- end
690
- ```
691
-
692
- ## Contributing
693
-
694
- Contributions are welcome! Please fork this repo and send a pull request. Your pull request should have:
695
-
696
- * a description about what's broken or what the desired functionality is
697
- * a test illustrating the bug or new feature
698
- * the code to fix the bug
699
-
700
- Ideally, the PR has 2 commits - the first showing the failed test and the second with the fix - although this is not
701
- required. The commits will be squashed into master once accepted.
702
-
703
- ## Changelog
704
-
705
- See [changelog](https://github.com/JsonApiClient/json_api_client/blob/master/CHANGELOG.md)
1
+ # JsonApiClient [![Build Status](https://travis-ci.org/JsonApiClient/json_api_client.png?branch=master)](https://travis-ci.org/JsonApiClient/json_api_client) [![Code Climate](https://codeclimate.com/github/JsonApiClient/json_api_client.png)](https://codeclimate.com/github/JsonApiClient/json_api_client) [![Code Coverage](https://codeclimate.com/github/JsonApiClient/json_api_client/coverage.png)](https://codeclimate.com/github/JsonApiClient/json_api_client)
2
+
3
+ This gem is meant to help you build an API client for interacting with REST APIs as laid out by [http://jsonapi.org](http://jsonapi.org). It attempts to give you a query building framework that is easy to understand (it is similar to ActiveRecord scopes).
4
+
5
+ *Note: master is currently tracking the 1.0.0 specification. If you're looking for the older code, see [0.x branch](https://github.com/JsonApiClient/json_api_client/tree/0.x)*
6
+
7
+ ## Usage
8
+
9
+ You will want to create your own resource classes that inherit from `JsonApiClient::Resource` similar to how you would create an `ActiveRecord` class. You may also want to create your own abstract base class to share common behavior. Additionally, you will probably want to namespace your models. Namespacing your model will not affect the url routing to that resource.
10
+
11
+ ```ruby
12
+ module MyApi
13
+ # this is an "abstract" base class that
14
+ class Base < JsonApiClient::Resource
15
+ # set the api base url in an abstract base class
16
+ self.site = "http://example.com/"
17
+ end
18
+
19
+ class Article < Base
20
+ end
21
+
22
+ class Comment < Base
23
+ end
24
+
25
+ class Person < Base
26
+ end
27
+ end
28
+ ```
29
+
30
+ By convention, we guess the resource route from the class name. In the above example, `Article`'s path is "http://example.com/articles" and `Person`'s path would be "http://example.com/people".
31
+
32
+ Some basic example usage:
33
+
34
+ ```ruby
35
+ MyApi::Article.all
36
+ MyApi::Article.where(author_id: 1).find(2)
37
+ MyApi::Article.where(author_id: 1).all
38
+
39
+ MyApi::Person.where(name: "foo").order(created_at: :desc).includes(:preferences, :cars).all
40
+
41
+ u = MyApi::Person.new(first_name: "bar", last_name: "foo")
42
+ u.new_record?
43
+ # => true
44
+ u.save
45
+
46
+ u.new_record?
47
+ # => false
48
+
49
+ u = MyApi::Person.find(1).first
50
+ u.update_attributes(
51
+ a: "b",
52
+ c: "d"
53
+ )
54
+
55
+ u.persisted?
56
+ # => true
57
+
58
+ u.destroy
59
+
60
+ u.destroyed?
61
+ # => true
62
+ u.persisted?
63
+ # => false
64
+
65
+ u = MyApi::Person.create(
66
+ a: "b",
67
+ c: "d"
68
+ )
69
+ ```
70
+
71
+ All class level finders/creators should return a `JsonApiClient::ResultSet` which behaves like an Array and contains extra data about the api response.
72
+
73
+
74
+ ## Handling Validation Errors
75
+
76
+ [See specification](http://jsonapi.org/format/#errors)
77
+
78
+ Out of the box, `json_api_client` handles server side validation only.
79
+
80
+ ```ruby
81
+ User.create(name: "Bob", email_address: "invalid email")
82
+ # => false
83
+
84
+ user = User.new(name: "Bob", email_address: "invalid email")
85
+ user.save
86
+ # => false
87
+
88
+ # returns an error collector which is array-like
89
+ user.errors
90
+ # => ["Email address is invalid"]
91
+
92
+ # get all error titles
93
+ user.errors.full_messages
94
+ # => ["Email address is invalid"]
95
+
96
+ # get errors for a specific parameter
97
+ user.errors[:email_address]
98
+ # => ["Email address is invalid"]
99
+
100
+ user = User.find(1)
101
+ user.update_attributes(email_address: "invalid email")
102
+ # => false
103
+
104
+ user.errors
105
+ # => ["Email address is invalid"]
106
+
107
+ user.email_address
108
+ # => "invalid email"
109
+ ```
110
+
111
+ For now we are assuming that error sources are all parameters.
112
+
113
+ If you want to add client side validation, I suggest creating a form model class that uses ActiveModel's validations.
114
+
115
+ ## Meta information
116
+
117
+ [See specification](http://jsonapi.org/format/#document-structure-meta)
118
+
119
+ If the response has a top level meta data section, we can access it via the `meta` accessor on `ResultSet`.
120
+
121
+ ```ruby
122
+ # Example response:
123
+ {
124
+ "meta": {
125
+ "copyright": "Copyright 2015 Example Corp.",
126
+ "authors": [
127
+ "Yehuda Katz",
128
+ "Steve Klabnik",
129
+ "Dan Gebhardt"
130
+ ]
131
+ },
132
+ "data": {
133
+ // ...
134
+ }
135
+ }
136
+ articles = Articles.all
137
+
138
+ articles.meta.copyright
139
+ # => "Copyright 2015 Example Corp."
140
+
141
+ articles.meta.authors
142
+ # => ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt"]
143
+ ```
144
+
145
+ ## Top-level Links
146
+
147
+ [See specification](http://jsonapi.org/format/#document-structure-top-level-links)
148
+
149
+ If the resource returns top level links, we can access them via the `links` accessor on `ResultSet`.
150
+
151
+ ```ruby
152
+ articles = Articles.find(1)
153
+ articles.links.related
154
+ ```
155
+
156
+ ## Nested Resources
157
+
158
+ You can force nested resource paths for your models by using a `belongs_to` association.
159
+
160
+ **Note: Using belongs_to is only necessary for setting a nested path unless you provide `shallow_path: true` option.**
161
+
162
+ ```ruby
163
+ module MyApi
164
+ class Account < JsonApiClient::Resource
165
+ belongs_to :user
166
+ end
167
+
168
+ class Customer < JsonApiClient::Resource
169
+ belongs_to :user, shallow_path: true
170
+ end
171
+ end
172
+
173
+ # try to find without the nested parameter
174
+ MyApi::Account.find(1)
175
+ # => raises ArgumentError
176
+
177
+ # makes request to /users/2/accounts/1
178
+ MyApi::Account.where(user_id: 2).find(1)
179
+ # => returns ResultSet
180
+
181
+ # makes request to /customers/1
182
+ MyApi::Customer.find(1)
183
+ # => returns ResultSet
184
+
185
+ # makes request to /users/2/customers/1
186
+ MyApi::Customer.where(user_id: 2).find(1)
187
+ # => returns ResultSet
188
+ ```
189
+
190
+ you can also override param name for `belongs_to` association
191
+
192
+ ```ruby
193
+ module MyApi
194
+ class Account < JsonApiClient::Resource
195
+ belongs_to :user, param: :customer_id
196
+ end
197
+ end
198
+
199
+ # makes request to /users/2/accounts/1
200
+ MyApi::Account.where(customer_id: 2).find(1)
201
+ # => returns ResultSet
202
+ ```
203
+
204
+ ## Custom Methods
205
+
206
+ You can create custom methods on both collections (class method) and members (instance methods).
207
+
208
+ ```ruby
209
+ module MyApi
210
+ class User < JsonApiClient::Resource
211
+ # GET /users/search
212
+ custom_endpoint :search, on: :collection, request_method: :get
213
+
214
+ # PUT /users/:id/verify
215
+ custom_endpoint :verify, on: :member, request_method: :put
216
+ end
217
+ end
218
+
219
+ # makes GET request to /users/search?name=Jeff
220
+ MyApi::User.search(name: 'Jeff')
221
+ # => <ResultSet of MyApi::User instances>
222
+
223
+ user = MyApi::User.find(1)
224
+ # makes PUT request to /users/1/verify?foo=bar
225
+ user.verify(foo: 'bar')
226
+ ```
227
+
228
+ ## Fetching Includes
229
+
230
+ [See specification](http://jsonapi.org/format/#fetching-includes)
231
+
232
+ If the response returns a [compound document](http://jsonapi.org/format/#document-compound-documents), then we should be able to get the related resources.
233
+
234
+ ```ruby
235
+ # makes request to /articles/1?include=author,comments.author
236
+ results = Article.includes(:author, :comments => :author).find(1)
237
+
238
+ # should not have to make additional requests to the server
239
+ authors = results.map(&:author)
240
+
241
+ # makes POST request to /articles?include=author,comments.author
242
+ article = Article.new(title: 'New one').request_includes(:author, :comments => :author)
243
+ article.save
244
+
245
+ # makes PATCH request to /articles/1?include=author,comments.author
246
+ article = Article.find(1)
247
+ article.title = 'Changed'
248
+ article.request_includes(:author, :comments => :author)
249
+ article.save
250
+
251
+ # request includes will be cleared if response is successful
252
+ # to avoid this `keep_request_params` class attribute can be used
253
+ Article.keep_request_params = true
254
+
255
+ # to clear request_includes use
256
+ article.reset_request_includes!
257
+ ```
258
+
259
+ ## Sparse Fieldsets
260
+
261
+ [See specification](http://jsonapi.org/format/#fetching-sparse-fieldsets)
262
+
263
+ ```ruby
264
+ # makes request to /articles?fields[articles]=title,body
265
+ article = Article.select("title", "body").first
266
+
267
+ # should have fetched the requested fields
268
+ article.title
269
+ # => "Rails is Omakase"
270
+
271
+ # should not have returned the created_at
272
+ article.created_at
273
+ # => raise NoMethodError
274
+
275
+ # or you can use fieldsets from multiple resources
276
+ # makes request to /articles?fields[articles]=title,body&fields[comments]=tag
277
+ article = Article.select("title", "body",{comments: 'tag'}).first
278
+
279
+ # makes POST request to /articles?fields[articles]=title,body&fields[comments]=tag
280
+ article = Article.new(title: 'New one').request_select(:title, :body, comments: 'tag')
281
+ article.save
282
+
283
+ # makes PATCH request to /articles/1?fields[articles]=title,body&fields[comments]=tag
284
+ article = Article.find(1)
285
+ article.title = 'Changed'
286
+ article.request_select(:title, :body, comments: 'tag')
287
+ article.save
288
+
289
+ # request fields will be cleared if response is successful
290
+ # to avoid this `keep_request_params` class attribute can be used
291
+ Article.keep_request_params = true
292
+
293
+ # to clear request fields use
294
+ article.reset_request_select!(:comments) # to clear for comments
295
+ article.reset_request_select! # to clear for all fields
296
+ ```
297
+
298
+ ## Sorting
299
+
300
+ [See specification](http://jsonapi.org/format/#fetching-sorting)
301
+
302
+ ```ruby
303
+ # makes request to /people?sort=age
304
+ youngest = Person.order(:age).all
305
+
306
+ # also makes request to /people?sort=age
307
+ youngest = Person.order(age: :asc).all
308
+
309
+ # makes request to /people?sort=-age
310
+ oldest = Person.order(age: :desc).all
311
+ ```
312
+
313
+ ## Paginating
314
+
315
+ [See specification](http://jsonapi.org/format/#fetching-pagination)
316
+
317
+ ### Requesting
318
+
319
+ ```ruby
320
+ # makes request to /articles?page=2&per_page=30
321
+ articles = Article.page(2).per(30).to_a
322
+
323
+ # also makes request to /articles?page=2&per_page=30
324
+ articles = Article.paginate(page: 2, per_page: 30).to_a
325
+
326
+ # keep in mind that page number can be nil - in that case default number will be applied
327
+ # also makes request to /articles?page=1&per_page=30
328
+ articles = Article.paginate(page: nil, per_page: 30).to_a
329
+ ```
330
+
331
+ *Note: The mapping of pagination parameters is done by the `query_builder` which is [customizable](#custom-paginator).*
332
+
333
+ ### Browsing
334
+
335
+ If the response contains additional pagination links, you can also get at those:
336
+
337
+ ```ruby
338
+ articles = Article.paginate(page: 2, per_page: 30).to_a
339
+ articles.pages.next
340
+ articles.pages.last
341
+ ```
342
+
343
+ ### Library compatibility
344
+
345
+ A `JsonApiClient::ResultSet` object should be paginatable with both `kaminari` and `will_paginate`.
346
+
347
+ ## Filtering
348
+
349
+ [See specifiation](http://jsonapi.org/format/#fetching-filtering)
350
+
351
+ ```ruby
352
+ # makes request to /people?filter[name]=Jeff
353
+ Person.where(name: 'Jeff').all
354
+ ```
355
+
356
+ ## Schema
357
+
358
+ You can define schema within your client model. You can define basic types and set default values if you wish. If you declare a basic type, we will try to cast any input to be that type.
359
+
360
+ The added benefit of declaring your schema is that you can access fields before data is set (otherwise, you'll get a `NoMethodError`).
361
+
362
+ **Note: This is completely optional. This will set default values and handle typecasting.**
363
+
364
+ ### Example
365
+
366
+ ```ruby
367
+ class User < JsonApiClient::Resource
368
+ property :name, type: :string
369
+ property :is_admin, type: :boolean, default: false
370
+ property :points_accrued, type: :int, default: 0
371
+ property :averge_points_per_day, type: :float
372
+ end
373
+
374
+ # default values
375
+ u = User.new
376
+
377
+ u.name
378
+ # => nil
379
+
380
+ u.is_admin
381
+ # => false
382
+
383
+ u.points_accrued
384
+ # => 0
385
+
386
+ # casting
387
+ u.average_points_per_day = "0.3"
388
+ u.average_points_per_day
389
+ # => 0.3
390
+ ```
391
+
392
+ ### Types
393
+
394
+ The basic types that we allow are:
395
+
396
+ * `:int` or `:integer`
397
+ * `:float`
398
+ * `:string`
399
+ * `:time` - *Note: Include the time zone in the string if it's different than local time.
400
+ * `:boolean` - *Note: we will cast the string version of "true" and "false" to their respective values*
401
+
402
+ Also, we consider `nil` to be an acceptable value and will not cast the value.
403
+
404
+ Note : Do not map the primary key as int.
405
+
406
+ ## Customizing
407
+
408
+ ### Paths
409
+
410
+ You can customize this path by changing your resource's `table_name`:
411
+
412
+ ```ruby
413
+ module MyApi
414
+ class SomeResource < Base
415
+ def self.table_name
416
+ "foobar"
417
+ end
418
+ end
419
+ end
420
+
421
+ # requests http://example.com/foobar
422
+ MyApi::SomeResource.all
423
+ ```
424
+
425
+ ### Custom headers
426
+
427
+ You can inject custom headers on resource request by wrapping your code into block:
428
+ ```ruby
429
+ MyApi::SomeResource.with_headers(x_access_token: 'secure_token_here') do
430
+ MyApi::SomeResource.find(1)
431
+ end
432
+ ```
433
+
434
+ ### Connections
435
+
436
+ You can configure your API client to use a custom connection that implementes the `run` instance method. It should return data that your parser can handle. The default connection class wraps Faraday and lets you add middleware.
437
+
438
+ ```ruby
439
+ class NullConnection
440
+ def initialize(*args)
441
+ end
442
+
443
+ def run(request_method, path, params: nil, headers: {}, body: nil)
444
+ end
445
+
446
+ def use(*args); end
447
+ end
448
+
449
+ class CustomConnectionResource < TestResource
450
+ self.connection_class = NullConnection
451
+ end
452
+ ```
453
+
454
+ #### Connection Options
455
+
456
+ You can configure your connection using Faraday middleware. In general, you'll want
457
+ to do this in a base model that all your resources inherit from:
458
+
459
+ ```ruby
460
+ MyApi::Base.connection do |connection|
461
+ # set OAuth2 headers
462
+ connection.use FaradayMiddleware::OAuth2, 'MYTOKEN'
463
+
464
+ # log responses
465
+ connection.use Faraday::Response::Logger
466
+
467
+ connection.use MyCustomMiddleware
468
+ end
469
+
470
+ module MyApi
471
+ class User < Base
472
+ # will use the customized connection
473
+ end
474
+ end
475
+ ```
476
+
477
+ ##### Server errors handling
478
+
479
+ Non-success API response will cause the specific `JsonApiClient::Errors::SomeException` raised, depends on responded HTTP status.
480
+ Please refer to [JsonApiClient::Middleware::Status#handle_status](https://github.com/JsonApiClient/json_api_client/blob/master/lib/json_api_client/middleware/status.rb)
481
+ method for concrete status-to-exception mapping used out of the box.
482
+
483
+ JsonApiClient will try determine is failed API response JsonApi-compatible, if so - JsonApi error messages will be parsed from response body, and tracked as a part of particular exception message. In additional, `JsonApiClient::Errors::ServerError` exception will keep the actual HTTP status and message within its message.
484
+
485
+ ##### Custom status handler
486
+
487
+ You can change handling of response status using `connection_options`. For example you can override 400 status handling.
488
+ By default it raises `JsonApiClient::Errors::ClientError` but you can skip exception if you want to process errors from the server.
489
+ You need to provide a `proc` which should call `throw(:handled)` default handler for this status should be skipped.
490
+ ```ruby
491
+ class ApiBadRequestHandler
492
+ def self.call(_env)
493
+ # do not raise exception
494
+ end
495
+ end
496
+
497
+ class CustomUnauthorizedError < StandardError
498
+ attr_reader :env
499
+
500
+ def initialize(env)
501
+ @env = env
502
+ super('not authorized')
503
+ end
504
+ end
505
+
506
+ MyApi::Base.connection_options[:status_handlers] = {
507
+ 400 => ApiBadRequestHandler,
508
+ 401 => ->(env) { raise CustomUnauthorizedError, env }
509
+ }
510
+
511
+ module MyApi
512
+ class User < Base
513
+ # will use the customized status_handlers
514
+ end
515
+ end
516
+
517
+ user = MyApi::User.create(name: 'foo')
518
+ # server responds with { errors: [ { detail: 'bad request' } ] }
519
+ user.errors.messages # { base: ['bad request'] }
520
+ # on 401 it will raise CustomUnauthorizedError instead of JsonApiClient::Errors::NotAuthorized
521
+ ```
522
+
523
+ ##### Specifying an HTTP Proxy
524
+
525
+ All resources have a class method ```connection_options``` used to pass options to the JsonApiClient::Connection initializer.
526
+
527
+ ```ruby
528
+ MyApi::Base.connection_options[:proxy] = 'http://proxy.example.com'
529
+ MyApi::Base.connection do |connection|
530
+ # ...
531
+ end
532
+
533
+ module MyApi
534
+ class User < Base
535
+ # will use the customized connection with proxy
536
+ end
537
+ end
538
+ ```
539
+
540
+ ### Custom Parser
541
+
542
+ You can configure your API client to use a custom parser that implements the `parse` class method. It should return a `JsonApiClient::ResultSet` instance. You can use it by setting the parser attribute on your model:
543
+
544
+ ```ruby
545
+ class MyCustomParser
546
+ def self.parse(klass, response)
547
+ # …
548
+ # returns some ResultSet object
549
+ end
550
+ end
551
+
552
+ class MyApi::Base < JsonApiClient::Resource
553
+ self.parser = MyCustomParser
554
+ end
555
+ ```
556
+
557
+ ### Custom Query Builder
558
+
559
+ You can customize how the scope builder methods map to request parameters.
560
+
561
+ ```ruby
562
+ class MyQueryBuilder
563
+ def initialize(klass); end
564
+
565
+ def where(conditions = {})
566
+ end
567
+
568
+ # … add order, includes, paginate, page, first, build
569
+ end
570
+
571
+ class MyApi::Base < JsonApiClient::Resource
572
+ self.query_builder = MyQueryBuilder
573
+ end
574
+ ```
575
+
576
+ ### Custom Paginator
577
+
578
+ You can customize how your resources find pagination information from the response.
579
+
580
+ If the [existing paginator](https://github.com/JsonApiClient/json_api_client/blob/master/lib/json_api_client/paginating/paginator.rb) fits your requirements but you don't use the default `page` and `per_page` params for pagination, you can customise the param keys as follows:
581
+
582
+ ```ruby
583
+ JsonApiClient::Paginating::Paginator.page_param = "number"
584
+ JsonApiClient::Paginating::Paginator.per_page_param = "size"
585
+ ```
586
+
587
+ Please note that this is a global configuration, so library authors should create a custom paginator that inherits `JsonApiClient::Paginating::Paginator` and configure the custom paginator to avoid modifying global config.
588
+
589
+ If the [existing paginator](https://github.com/JsonApiClient/json_api_client/blob/master/lib/json_api_client/paginating/paginator.rb) does not fit your needs, you can create a custom paginator:
590
+
591
+ ```ruby
592
+ class MyPaginator
593
+ def initialize(result_set, data); end
594
+ # implement current_page, total_entries, etc
595
+ end
596
+
597
+ class MyApi::Base < JsonApiClient::Resource
598
+ self.paginator = MyPaginator
599
+ end
600
+ ```
601
+
602
+ ### NestedParamPaginator
603
+
604
+ The default `JsonApiClient::Paginating::Paginator` is not strict about how it handles the param keys ([#347](https://github.com/JsonApiClient/json_api_client/issues/347)). There is a second paginator that more rigorously adheres to the JSON:API pagination recommendation style of `page[page]=1&page[per_page]=10`.
605
+
606
+ If this second style suits your needs better, it is available as a class override:
607
+
608
+ ```ruby
609
+ class Order < JsonApiClient::Resource
610
+ self.paginator = JsonApiClient::Paginating::NestedParamPaginator
611
+ end
612
+ ```
613
+
614
+ You can also extend `NestedParamPaginator` in your custom paginators or assign the `page_param` or `per_page_param` as with the default version above.
615
+
616
+ ### Custom type
617
+
618
+ If your model must be named differently from classified type of resource you can easily customize it.
619
+ It will work both for defined and not defined relationships
620
+
621
+ ```ruby
622
+ class MyApi::Base < JsonApiClient::Resource
623
+ resolve_custom_type 'document--files', 'File'
624
+ end
625
+
626
+ class MyApi::File < MyApi::Base
627
+ def self.resource_name
628
+ 'document--files'
629
+ end
630
+ end
631
+ ```
632
+
633
+ ### Type Casting
634
+
635
+ You can define your own types and its casting mechanism for schema.
636
+
637
+ ```ruby
638
+ require 'money'
639
+ class MyMoneyCaster
640
+ def self.cast(value, default)
641
+ begin
642
+ Money.new(value, "USD")
643
+ rescue ArgumentError
644
+ default
645
+ end
646
+ end
647
+ end
648
+
649
+ JsonApiClient::Schema.register money: MyMoneyCaster
650
+
651
+ ```
652
+ and finally
653
+
654
+ ```ruby
655
+ class Order < JsonApiClient::Resource
656
+ property :total_amount, type: :money
657
+ end
658
+
659
+ ```
660
+
661
+ ### Safe singular resource fetching
662
+
663
+ That is a bit curios, but `json_api_client` returns an array from `.find` method, always.
664
+ The history of this fact was discussed [here](https://github.com/JsonApiClient/json_api_client/issues/75)
665
+
666
+ So, when we searching for a single resource by primary key, we typically write the things like
667
+
668
+ ```ruby
669
+ admin = User.find(id).first
670
+ ```
671
+
672
+ The next thing which we need to notice - `json_api_client` will just interpolate the incoming `.find` param to the end of API URL, just like that:
673
+
674
+ > http://somehost/api/v1/users/{id}
675
+
676
+ What will happen if we pass the blank id (nil or empty string) to the `.find` method then?.. Yeah, `json_api_client` will try to call the INDEX API endpoint instead of SHOW one:
677
+
678
+ > http://somehost/api/v1/users/
679
+
680
+ Lets sum all together - in case if `id` comes blank (from CGI for instance), we can silently receive the `admin` variable equal to some existing resource, with all the consequences.
681
+
682
+ Even worse, `admin` variable can equal to *random* resource, depends on ordering applied by INDEX endpoint.
683
+
684
+ If you prefer to get `JsonApiClient::Errors::NotFound` raised, please define in your base Resource class:
685
+
686
+ ```ruby
687
+ class Resource < JsonApiClient::Resource
688
+ self.raise_on_blank_find_param = true
689
+ end
690
+ ```
691
+
692
+ ## Contributing
693
+
694
+ Contributions are welcome! Please fork this repo and send a pull request. Your pull request should have:
695
+
696
+ * a description about what's broken or what the desired functionality is
697
+ * a test illustrating the bug or new feature
698
+ * the code to fix the bug
699
+
700
+ Ideally, the PR has 2 commits - the first showing the failed test and the second with the fix - although this is not
701
+ required. The commits will be squashed into master once accepted.
702
+
703
+ ### Set up development locally
704
+
705
+ - With a decent ruby version:
706
+ ```shell
707
+ bundle install
708
+ appraisal install
709
+ ```
710
+
711
+ - Run all tests for that ruby version:
712
+ ```shell
713
+ appraisal rake
714
+ ```
715
+
716
+ - For more info on appraisal:
717
+ ```shell
718
+ appraisal help
719
+ ```
720
+
721
+ ## Changelog
722
+
723
+ See [changelog](https://github.com/JsonApiClient/json_api_client/blob/master/CHANGELOG.md)