json_api_client 1.5.2 → 1.22.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.
- checksums.yaml +5 -5
- data/README.md +206 -9
- data/lib/json_api_client/associations/base_association.rb +7 -0
- data/lib/json_api_client/associations/belongs_to.rb +11 -10
- data/lib/json_api_client/associations/has_many.rb +0 -8
- data/lib/json_api_client/associations/has_one.rb +6 -9
- data/lib/json_api_client/connection.rb +8 -3
- data/lib/json_api_client/error_collector.rb +19 -10
- data/lib/json_api_client/errors.rb +86 -17
- data/lib/json_api_client/helpers/associatable.rb +88 -0
- data/lib/json_api_client/helpers/dirty.rb +5 -1
- data/lib/json_api_client/helpers/dynamic_attributes.rb +19 -11
- data/lib/json_api_client/helpers.rb +1 -0
- data/lib/json_api_client/included_data.rb +24 -12
- data/lib/json_api_client/middleware/json_request.rb +16 -1
- data/lib/json_api_client/middleware/status.rb +32 -6
- data/lib/json_api_client/paginating/nested_param_paginator.rb +140 -0
- data/lib/json_api_client/paginating/paginator.rb +1 -1
- data/lib/json_api_client/paginating.rb +2 -1
- data/lib/json_api_client/query/builder.rb +77 -49
- data/lib/json_api_client/query/requestor.rb +21 -13
- data/lib/json_api_client/relationships/relations.rb +0 -1
- data/lib/json_api_client/request_params.rb +57 -0
- data/lib/json_api_client/resource.rb +196 -46
- data/lib/json_api_client/schema.rb +3 -3
- data/lib/json_api_client/utils.rb +26 -1
- data/lib/json_api_client/version.rb +1 -1
- data/lib/json_api_client.rb +2 -1
- metadata +44 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 50ee381b7fb9cdebf8538c162a5dab717e8e2e23fffdc293ef7738728f45f9c3
|
|
4
|
+
data.tar.gz: 1be6e62baf09fc3bc78c7c65a5a7b567b0c4a875d1e6bbe06411774ee34d059a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60b1b657a5b30630f03e461bc4d29f5c0127417c53d63da146059d7a4bfef97e294d48d3bea842811dc72366e18cca7b1ddb1f68a573d42f1571a30dc199a9f6
|
|
7
|
+
data.tar.gz: a99ff29dd85edb6e036538c61d9ae84359a62bd8464e4f9839c09e2d8c1859d30ee58182a962503b00134fcb20c473520157bbe52b0dba5819dfc70402054d4b
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# JsonApiClient [](https://travis-ci.org/JsonApiClient/json_api_client) [](https://codeclimate.com/github/JsonApiClient/json_api_client) [](https://codeclimate.com/github/JsonApiClient/json_api_client)
|
|
2
2
|
|
|
3
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
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/
|
|
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
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
@@ -39,14 +39,29 @@ MyApi::Article.where(author_id: 1).all
|
|
|
39
39
|
MyApi::Person.where(name: "foo").order(created_at: :desc).includes(:preferences, :cars).all
|
|
40
40
|
|
|
41
41
|
u = MyApi::Person.new(first_name: "bar", last_name: "foo")
|
|
42
|
+
u.new_record?
|
|
43
|
+
# => true
|
|
42
44
|
u.save
|
|
43
45
|
|
|
46
|
+
u.new_record?
|
|
47
|
+
# => false
|
|
48
|
+
|
|
44
49
|
u = MyApi::Person.find(1).first
|
|
45
50
|
u.update_attributes(
|
|
46
51
|
a: "b",
|
|
47
52
|
c: "d"
|
|
48
53
|
)
|
|
49
54
|
|
|
55
|
+
u.persisted?
|
|
56
|
+
# => true
|
|
57
|
+
|
|
58
|
+
u.destroy
|
|
59
|
+
|
|
60
|
+
u.destroyed?
|
|
61
|
+
# => true
|
|
62
|
+
u.persisted?
|
|
63
|
+
# => false
|
|
64
|
+
|
|
50
65
|
u = MyApi::Person.create(
|
|
51
66
|
a: "b",
|
|
52
67
|
c: "d"
|
|
@@ -142,13 +157,17 @@ articles.links.related
|
|
|
142
157
|
|
|
143
158
|
You can force nested resource paths for your models by using a `belongs_to` association.
|
|
144
159
|
|
|
145
|
-
**Note: Using belongs_to is only necessary for setting a nested path.**
|
|
160
|
+
**Note: Using belongs_to is only necessary for setting a nested path unless you provide `shallow_path: true` option.**
|
|
146
161
|
|
|
147
162
|
```ruby
|
|
148
163
|
module MyApi
|
|
149
164
|
class Account < JsonApiClient::Resource
|
|
150
165
|
belongs_to :user
|
|
151
166
|
end
|
|
167
|
+
|
|
168
|
+
class Customer < JsonApiClient::Resource
|
|
169
|
+
belongs_to :user, shallow_path: true
|
|
170
|
+
end
|
|
152
171
|
end
|
|
153
172
|
|
|
154
173
|
# try to find without the nested parameter
|
|
@@ -158,6 +177,28 @@ MyApi::Account.find(1)
|
|
|
158
177
|
# makes request to /users/2/accounts/1
|
|
159
178
|
MyApi::Account.where(user_id: 2).find(1)
|
|
160
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
|
|
161
202
|
```
|
|
162
203
|
|
|
163
204
|
## Custom Methods
|
|
@@ -196,6 +237,23 @@ results = Article.includes(:author, :comments => :author).find(1)
|
|
|
196
237
|
|
|
197
238
|
# should not have to make additional requests to the server
|
|
198
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!
|
|
199
257
|
```
|
|
200
258
|
|
|
201
259
|
## Sparse Fieldsets
|
|
@@ -217,6 +275,24 @@ article.created_at
|
|
|
217
275
|
# or you can use fieldsets from multiple resources
|
|
218
276
|
# makes request to /articles?fields[articles]=title,body&fields[comments]=tag
|
|
219
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
|
|
220
296
|
```
|
|
221
297
|
|
|
222
298
|
## Sorting
|
|
@@ -246,6 +322,10 @@ articles = Article.page(2).per(30).to_a
|
|
|
246
322
|
|
|
247
323
|
# also makes request to /articles?page=2&per_page=30
|
|
248
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
|
|
249
329
|
```
|
|
250
330
|
|
|
251
331
|
*Note: The mapping of pagination parameters is done by the `query_builder` which is [customizable](#custom-paginator).*
|
|
@@ -360,7 +440,7 @@ class NullConnection
|
|
|
360
440
|
def initialize(*args)
|
|
361
441
|
end
|
|
362
442
|
|
|
363
|
-
def run(request_method, path, params
|
|
443
|
+
def run(request_method, path, params: nil, headers: {}, body: nil)
|
|
364
444
|
end
|
|
365
445
|
|
|
366
446
|
def use(*args); end
|
|
@@ -394,6 +474,52 @@ module MyApi
|
|
|
394
474
|
end
|
|
395
475
|
```
|
|
396
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
|
+
|
|
397
523
|
##### Specifying an HTTP Proxy
|
|
398
524
|
|
|
399
525
|
All resources have a class method ```connection_options``` used to pass options to the JsonApiClient::Connection initializer.
|
|
@@ -451,16 +577,16 @@ end
|
|
|
451
577
|
|
|
452
578
|
You can customize how your resources find pagination information from the response.
|
|
453
579
|
|
|
454
|
-
If the [existing paginator](https://github.com/
|
|
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:
|
|
455
581
|
|
|
456
582
|
```ruby
|
|
457
|
-
JsonApiClient::Paginating::Paginator.page_param = "
|
|
458
|
-
JsonApiClient::Paginating::Paginator.per_page_param = "
|
|
583
|
+
JsonApiClient::Paginating::Paginator.page_param = "number"
|
|
584
|
+
JsonApiClient::Paginating::Paginator.per_page_param = "size"
|
|
459
585
|
```
|
|
460
586
|
|
|
461
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.
|
|
462
588
|
|
|
463
|
-
If the [existing paginator](https://github.com/
|
|
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:
|
|
464
590
|
|
|
465
591
|
```ruby
|
|
466
592
|
class MyPaginator
|
|
@@ -473,6 +599,36 @@ class MyApi::Base < JsonApiClient::Resource
|
|
|
473
599
|
end
|
|
474
600
|
```
|
|
475
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
|
+
```
|
|
476
632
|
|
|
477
633
|
### Type Casting
|
|
478
634
|
|
|
@@ -502,7 +658,48 @@ end
|
|
|
502
658
|
|
|
503
659
|
```
|
|
504
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.
|
|
505
702
|
|
|
506
703
|
## Changelog
|
|
507
704
|
|
|
508
|
-
See [changelog](https://github.com/
|
|
705
|
+
See [changelog](https://github.com/JsonApiClient/json_api_client/blob/master/CHANGELOG.md)
|
|
@@ -21,6 +21,13 @@ module JsonApiClient
|
|
|
21
21
|
def from_result_set(result_set)
|
|
22
22
|
result_set.to_a
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
def load_records(data)
|
|
26
|
+
data.map do |d|
|
|
27
|
+
record_class = Utils.compute_type(klass, klass.key_formatter.unformat(d["type"]).classify)
|
|
28
|
+
record_class.load id: d["id"]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
24
31
|
end
|
|
25
32
|
end
|
|
26
33
|
end
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
module JsonApiClient
|
|
2
2
|
module Associations
|
|
3
3
|
module BelongsTo
|
|
4
|
-
|
|
4
|
+
class Association < BaseAssociation
|
|
5
|
+
include Helpers::URI
|
|
6
|
+
|
|
7
|
+
attr_reader :param
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
def initialize(attr_name, klass, options = {})
|
|
10
|
+
super
|
|
11
|
+
@param = options.fetch(:param, :"#{attr_name}_id").to_sym
|
|
12
|
+
@shallow_path = options.fetch(:shallow_path, false)
|
|
10
13
|
end
|
|
11
|
-
end
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def param
|
|
16
|
-
:"#{attr_name}_id"
|
|
15
|
+
def shallow_path?
|
|
16
|
+
@shallow_path
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def to_prefix_path(formatter)
|
|
@@ -21,6 +21,7 @@ module JsonApiClient
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def set_prefix_path(attrs, formatter)
|
|
24
|
+
return if shallow_path? && !attrs[param]
|
|
24
25
|
attrs[param] = encode_part(attrs[param]) if attrs.key?(param)
|
|
25
26
|
to_prefix_path(formatter) % attrs
|
|
26
27
|
end
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
module JsonApiClient
|
|
2
2
|
module Associations
|
|
3
3
|
module HasMany
|
|
4
|
-
extend ActiveSupport::Concern
|
|
5
|
-
|
|
6
|
-
module ClassMethods
|
|
7
|
-
def has_many(attr_name, options = {})
|
|
8
|
-
self.associations = self.associations + [HasMany::Association.new(attr_name, self, options)]
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
4
|
class Association < BaseAssociation
|
|
13
5
|
end
|
|
14
6
|
end
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
module JsonApiClient
|
|
2
2
|
module Associations
|
|
3
3
|
module HasOne
|
|
4
|
-
extend ActiveSupport::Concern
|
|
5
|
-
|
|
6
|
-
module ClassMethods
|
|
7
|
-
def has_one(attr_name, options = {})
|
|
8
|
-
self.associations += [HasOne::Association.new(attr_name, self, options)]
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
4
|
class Association < BaseAssociation
|
|
13
5
|
def from_result_set(result_set)
|
|
14
6
|
result_set.first
|
|
15
7
|
end
|
|
8
|
+
|
|
9
|
+
def load_records(data)
|
|
10
|
+
record_class = Utils.compute_type(klass, klass.key_formatter.unformat(data["type"]).classify)
|
|
11
|
+
record_class.load id: data["id"]
|
|
12
|
+
end
|
|
16
13
|
end
|
|
17
14
|
end
|
|
18
15
|
end
|
|
19
|
-
end
|
|
16
|
+
end
|
|
@@ -7,11 +7,14 @@ module JsonApiClient
|
|
|
7
7
|
site = options.fetch(:site)
|
|
8
8
|
connection_options = options.slice(:proxy, :ssl, :request, :headers, :params)
|
|
9
9
|
adapter_options = Array(options.fetch(:adapter, Faraday.default_adapter))
|
|
10
|
+
status_middleware_options = {}
|
|
11
|
+
status_middleware_options[:custom_handlers] = options[:status_handlers] if options[:status_handlers].present?
|
|
10
12
|
@faraday = Faraday.new(site, connection_options) do |builder|
|
|
11
13
|
builder.request :json
|
|
12
14
|
builder.use Middleware::JsonRequest
|
|
13
|
-
builder.use Middleware::Status
|
|
15
|
+
builder.use Middleware::Status, status_middleware_options
|
|
14
16
|
builder.use Middleware::ParseJson
|
|
17
|
+
builder.use ::FaradayMiddleware::Gzip
|
|
15
18
|
builder.adapter(*adapter_options)
|
|
16
19
|
end
|
|
17
20
|
yield(self) if block_given?
|
|
@@ -28,8 +31,10 @@ module JsonApiClient
|
|
|
28
31
|
faraday.builder.delete(middleware)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
def run(request_method, path, params
|
|
32
|
-
faraday.
|
|
34
|
+
def run(request_method, path, params: nil, headers: {}, body: nil)
|
|
35
|
+
faraday.run_request(request_method, path, body, headers) do |request|
|
|
36
|
+
request.params.update(params) if params
|
|
37
|
+
end
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
end
|
|
@@ -33,18 +33,27 @@ module JsonApiClient
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def source_parameter
|
|
36
|
-
source
|
|
37
|
-
source[:pointer] ?
|
|
38
|
-
source[:pointer].split("/").last :
|
|
39
|
-
nil
|
|
40
|
-
end
|
|
36
|
+
source[:parameter]
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
def source_pointer
|
|
44
|
-
source
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
source[:pointer]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error_key
|
|
44
|
+
if source_pointer && source_pointer != "/data"
|
|
45
|
+
source_pointer.split("/").last
|
|
46
|
+
else
|
|
47
|
+
"base"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error_msg
|
|
52
|
+
msg = title || detail || "invalid"
|
|
53
|
+
if source_parameter
|
|
54
|
+
"#{source_parameter} #{msg}"
|
|
55
|
+
else
|
|
56
|
+
msg
|
|
48
57
|
end
|
|
49
58
|
end
|
|
50
59
|
|
|
@@ -74,7 +83,7 @@ module JsonApiClient
|
|
|
74
83
|
|
|
75
84
|
def [](source)
|
|
76
85
|
map do |error|
|
|
77
|
-
error.
|
|
86
|
+
error.error_key == source
|
|
78
87
|
end
|
|
79
88
|
end
|
|
80
89
|
|
|
@@ -1,44 +1,103 @@
|
|
|
1
|
+
require 'rack'
|
|
2
|
+
|
|
1
3
|
module JsonApiClient
|
|
2
4
|
module Errors
|
|
3
5
|
class ApiError < StandardError
|
|
4
6
|
attr_reader :env
|
|
5
|
-
|
|
7
|
+
|
|
8
|
+
def initialize(env, msg = nil)
|
|
6
9
|
@env = env
|
|
10
|
+
# Try to fetch json_api errors from response
|
|
11
|
+
msg = track_json_api_errors(msg)
|
|
12
|
+
|
|
13
|
+
super msg
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Try to fetch json_api errors from response
|
|
19
|
+
def track_json_api_errors(msg)
|
|
20
|
+
return msg unless env.try(:body).kind_of?(Hash) || env.body.key?('errors')
|
|
21
|
+
|
|
22
|
+
errors_msg = env.body['errors'].map { |e| e['title'] }.compact.join('; ').presence
|
|
23
|
+
return msg unless errors_msg
|
|
24
|
+
|
|
25
|
+
msg.nil? ? errors_msg : "#{msg} (#{errors_msg})"
|
|
26
|
+
# Just to be sure that it is back compatible
|
|
27
|
+
rescue StandardError
|
|
28
|
+
msg
|
|
7
29
|
end
|
|
8
30
|
end
|
|
9
31
|
|
|
10
32
|
class ClientError < ApiError
|
|
11
33
|
end
|
|
12
34
|
|
|
35
|
+
class ResourceImmutableError < StandardError
|
|
36
|
+
def initialize(msg = 'Resource immutable')
|
|
37
|
+
super msg
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
13
41
|
class AccessDenied < ClientError
|
|
14
42
|
end
|
|
15
43
|
|
|
16
44
|
class NotAuthorized < ClientError
|
|
17
45
|
end
|
|
18
46
|
|
|
47
|
+
class NotFound < ClientError
|
|
48
|
+
attr_reader :uri
|
|
49
|
+
def initialize(env_or_uri, msg = nil)
|
|
50
|
+
env = nil
|
|
51
|
+
|
|
52
|
+
if env_or_uri.kind_of?(Faraday::Env)
|
|
53
|
+
env = env_or_uri
|
|
54
|
+
@uri = env[:url]
|
|
55
|
+
else
|
|
56
|
+
@uri = env_or_uri
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
msg ||= "Resource not found: #{uri.to_s}"
|
|
60
|
+
super env, msg
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class RequestTimeout < ClientError
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Conflict < ClientError
|
|
68
|
+
def initialize(env, msg = 'Resource already exists')
|
|
69
|
+
super env, msg
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class TooManyRequests < ClientError
|
|
74
|
+
end
|
|
75
|
+
|
|
19
76
|
class ConnectionError < ApiError
|
|
20
77
|
end
|
|
21
78
|
|
|
22
79
|
class ServerError < ApiError
|
|
23
|
-
def
|
|
24
|
-
|
|
80
|
+
def initialize(env, msg = nil)
|
|
81
|
+
msg ||= begin
|
|
82
|
+
status = env.status
|
|
83
|
+
message = ::Rack::Utils::HTTP_STATUS_CODES[status]
|
|
84
|
+
"#{status} #{message}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
super env, msg
|
|
25
88
|
end
|
|
26
89
|
end
|
|
27
90
|
|
|
28
|
-
class
|
|
29
|
-
def message
|
|
30
|
-
"Resource already exists"
|
|
31
|
-
end
|
|
91
|
+
class InternalServerError < ServerError
|
|
32
92
|
end
|
|
33
93
|
|
|
34
|
-
class
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end
|
|
94
|
+
class BadGateway < ServerError
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class ServiceUnavailable < ServerError
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class GatewayTimeout < ServerError
|
|
42
101
|
end
|
|
43
102
|
|
|
44
103
|
class UnexpectedStatus < ServerError
|
|
@@ -46,11 +105,21 @@ module JsonApiClient
|
|
|
46
105
|
def initialize(code, uri)
|
|
47
106
|
@code = code
|
|
48
107
|
@uri = uri
|
|
108
|
+
|
|
109
|
+
msg = "Unexpected response status: #{code} from: #{uri.to_s}"
|
|
110
|
+
super nil, msg
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class RecordNotSaved < ServerError
|
|
115
|
+
attr_reader :record
|
|
116
|
+
|
|
117
|
+
def initialize(message = nil, record = nil)
|
|
118
|
+
@record = record
|
|
49
119
|
end
|
|
50
120
|
def message
|
|
51
|
-
"
|
|
121
|
+
"Record not saved"
|
|
52
122
|
end
|
|
53
123
|
end
|
|
54
|
-
|
|
55
124
|
end
|
|
56
125
|
end
|