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