json_api_client 0.9.6 → 1.0.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +274 -84
- data/lib/json_api_client.rb +9 -4
- data/lib/json_api_client/connection.rb +5 -5
- data/lib/json_api_client/error_collector.rb +29 -0
- data/lib/json_api_client/errors.rb +7 -5
- data/lib/json_api_client/helpers.rb +3 -0
- data/lib/json_api_client/helpers/associable.rb +4 -3
- data/lib/json_api_client/helpers/attributable.rb +15 -46
- data/lib/json_api_client/helpers/custom_endpoints.rb +3 -10
- data/lib/json_api_client/helpers/dynamic_attributes.rb +61 -0
- data/lib/json_api_client/helpers/linkable.rb +28 -18
- data/lib/json_api_client/helpers/paginatable.rb +13 -0
- data/lib/json_api_client/helpers/parsable.rb +1 -1
- data/lib/json_api_client/helpers/queryable.rb +4 -3
- data/lib/json_api_client/helpers/requestable.rb +60 -0
- data/lib/json_api_client/linking.rb +7 -0
- data/lib/json_api_client/linking/included_data.rb +40 -0
- data/lib/json_api_client/linking/links.rb +29 -0
- data/lib/json_api_client/linking/top_level_links.rb +30 -0
- data/lib/json_api_client/meta_data.rb +10 -0
- data/lib/json_api_client/middleware/json_request.rb +3 -4
- data/lib/json_api_client/middleware/status.rb +10 -1
- data/lib/json_api_client/paginating.rb +5 -0
- data/lib/json_api_client/paginating/paginator.rb +80 -0
- data/lib/json_api_client/parsers.rb +5 -0
- data/lib/json_api_client/parsers/parser.rb +55 -0
- data/lib/json_api_client/query.rb +2 -7
- data/lib/json_api_client/query/builder.rb +126 -0
- data/lib/json_api_client/query/requestor.rb +77 -0
- data/lib/json_api_client/resource.rb +3 -59
- data/lib/json_api_client/result_set.rb +11 -29
- data/lib/json_api_client/schema.rb +15 -30
- data/lib/json_api_client/version.rb +1 -1
- metadata +36 -19
- data/lib/json_api_client/link.rb +0 -11
- data/lib/json_api_client/link_definition.rb +0 -27
- data/lib/json_api_client/linked_data.rb +0 -75
- data/lib/json_api_client/parser.rb +0 -63
- data/lib/json_api_client/query/base.rb +0 -38
- data/lib/json_api_client/query/create.rb +0 -17
- data/lib/json_api_client/query/custom.rb +0 -22
- data/lib/json_api_client/query/destroy.rb +0 -12
- data/lib/json_api_client/query/find.rb +0 -19
- data/lib/json_api_client/query/linked.rb +0 -24
- data/lib/json_api_client/query/update.rb +0 -13
- data/lib/json_api_client/scope.rb +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cab5bf2208be347ffc15b03c0af11c4c21ace976
|
4
|
+
data.tar.gz: 98074a5fcacd34fb0a4dc946f8968957437b8f3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f091b91fbb8006898403e893f7c934c11b600a1d0ccdb7e22afd8cefae97edaa51a1d59c3e9a52e1cf9fb52ceb2708db23407138c2dff9837f7bfdfb165be34
|
7
|
+
data.tar.gz: 286263275aebe8fb7a777c817a3b368dda97bea2aaec6208061631774f98ec92f2b5ea6f5a9ddabd3764de34a941a23fedf90b1f9587a286acb2ba0e2194a75c
|
data/README.md
CHANGED
@@ -2,107 +2,66 @@
|
|
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 RC3 specification. If you're looking for the older code, see [0.x branch](https://github.com/chingor13/json_api_client/tree/0.x)*
|
6
|
+
|
5
7
|
*Note: This is still a work in progress.*
|
6
8
|
|
7
9
|
## Usage
|
8
10
|
|
11
|
+
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.
|
12
|
+
|
9
13
|
```
|
10
14
|
module MyApi
|
11
|
-
class
|
12
|
-
|
15
|
+
# this is an "abstract" base class that
|
16
|
+
class Base < JsonApiClient::Resource
|
17
|
+
# set the api base url in an abstract base class
|
18
|
+
self.site = "http://example.com/"
|
13
19
|
end
|
14
|
-
|
15
|
-
class
|
16
|
-
|
20
|
+
|
21
|
+
class Article < Base
|
22
|
+
end
|
23
|
+
|
24
|
+
class Comment < Base
|
25
|
+
end
|
26
|
+
|
27
|
+
class Person < Base
|
17
28
|
end
|
18
29
|
end
|
30
|
+
```
|
31
|
+
|
32
|
+
By convention, we figure 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".
|
19
33
|
|
20
|
-
|
21
|
-
MyApi::User.where(account_id: 1).find(1)
|
22
|
-
MyApi::User.where(account_id: 1).all
|
34
|
+
Some example usage:
|
23
35
|
|
24
|
-
|
36
|
+
```
|
37
|
+
MyApi::Article.all
|
38
|
+
MyApi::Article.where(author_id: 1).find(2)
|
39
|
+
MyApi::Article.where(author_id: 1).all
|
25
40
|
|
26
|
-
|
41
|
+
MyApi::Person.where(name: "foo").order(created_at: :desc).includes(:preferences, :cars).all
|
42
|
+
|
43
|
+
u = MyApi::Person.new(first_name: "bar", last_name: "foo")
|
27
44
|
u.save
|
28
45
|
|
29
|
-
u = MyApi::
|
46
|
+
u = MyApi::Person.find(1).first
|
30
47
|
u.update_attributes(
|
31
48
|
a: "b",
|
32
49
|
c: "d"
|
33
50
|
)
|
34
51
|
|
35
|
-
u = MyApi::
|
52
|
+
u = MyApi::Person.create(
|
36
53
|
a: "b",
|
37
54
|
c: "d"
|
38
55
|
)
|
39
|
-
|
40
|
-
u = MyApi::User.find(1).first
|
41
|
-
u.accounts
|
42
|
-
=> MyApi::Account.where(user_id: u.id).all
|
43
56
|
```
|
44
57
|
|
45
|
-
|
46
|
-
|
47
|
-
You can configure your connection using Faraday middleware. In general, you'll want
|
48
|
-
to do this in a base model that all your resources inherit from:
|
49
|
-
|
50
|
-
```
|
51
|
-
MyApi::Base.connection do |connection|
|
52
|
-
# set OAuth2 headers
|
53
|
-
connection.use Faraday::Request::Oauth2, 'MYTOKEN'
|
58
|
+
All class level finders/creators should return a `JsonApiClient::ResultSet` which behaves like an Array and contains extra data about the api response.
|
54
59
|
|
55
|
-
# log responses
|
56
|
-
connection.use Faraday::Response::Logger
|
57
|
-
|
58
|
-
connection.use MyCustomMiddleware
|
59
|
-
end
|
60
|
-
|
61
|
-
module MyApi
|
62
|
-
class User < Base
|
63
|
-
# will use the customized connection
|
64
|
-
end
|
65
|
-
end
|
66
|
-
```
|
67
|
-
|
68
|
-
## Custom Connection
|
69
|
-
|
70
|
-
You can configure your API client to use a custom connection that implementes the `execute` instance method. It should return data that your parser can handle.
|
71
|
-
|
72
|
-
```
|
73
|
-
class NullConnection
|
74
|
-
def initialize(*args)
|
75
|
-
end
|
76
|
-
|
77
|
-
def execute(query)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
class CustomConnectionResource < TestResource
|
82
|
-
self.connection_class = NullConnection
|
83
|
-
end
|
84
|
-
|
85
|
-
```
|
86
|
-
|
87
|
-
## Custom Parser
|
88
|
-
|
89
|
-
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:
|
90
|
-
|
91
|
-
```
|
92
|
-
class MyCustomParser
|
93
|
-
def self.parse(klass, response)
|
94
|
-
…
|
95
|
-
# returns some ResultSet object
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
class MyApi::Base < JsonApiClient::Resource
|
100
|
-
self.parser = MyCustomParser
|
101
|
-
end
|
102
|
-
```
|
103
60
|
|
104
61
|
## Handling Validation Errors
|
105
62
|
|
63
|
+
Out of the box, `json_api_client` handles server side validation only.
|
64
|
+
|
106
65
|
```
|
107
66
|
User.create(name: "Bob", email_address: "invalid email")
|
108
67
|
=> false
|
@@ -122,6 +81,48 @@ user.email_address
|
|
122
81
|
=> "invalid email"
|
123
82
|
```
|
124
83
|
|
84
|
+
If you want to add client side validation, I suggest creating a form model class that uses ActiveModel's validations.
|
85
|
+
|
86
|
+
## Meta information
|
87
|
+
|
88
|
+
[See specification](http://jsonapi.org/format/#document-structure-meta)
|
89
|
+
|
90
|
+
If the response has a top level meta data section, we can access it via the `meta` accessor on `ResultSet`.
|
91
|
+
|
92
|
+
```
|
93
|
+
# Example response:
|
94
|
+
{
|
95
|
+
"meta": {
|
96
|
+
"copyright": "Copyright 2015 Example Corp.",
|
97
|
+
"authors": [
|
98
|
+
"Yehuda Katz",
|
99
|
+
"Steve Klabnik",
|
100
|
+
"Dan Gebhardt"
|
101
|
+
]
|
102
|
+
},
|
103
|
+
"data": {
|
104
|
+
// ...
|
105
|
+
}
|
106
|
+
}
|
107
|
+
articles = Articles.all
|
108
|
+
|
109
|
+
articles.meta.copyright
|
110
|
+
=> "Copyright 2015 Example Corp."
|
111
|
+
articles.meta.authors
|
112
|
+
=> ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt"]
|
113
|
+
```
|
114
|
+
|
115
|
+
## Top-level Links
|
116
|
+
|
117
|
+
[See specification](http://jsonapi.org/format/#document-structure-top-level-links)
|
118
|
+
|
119
|
+
If the resource returns top level links, we can access them via the `links` accessor on `ResultSet`.
|
120
|
+
|
121
|
+
```
|
122
|
+
articles = Articles.find(1)
|
123
|
+
articles.links.related
|
124
|
+
```
|
125
|
+
|
125
126
|
## Nested Resources
|
126
127
|
|
127
128
|
You can force nested resource paths for your models by using a `belongs_to` association.
|
@@ -132,6 +133,14 @@ module MyApi
|
|
132
133
|
belongs_to :user
|
133
134
|
end
|
134
135
|
end
|
136
|
+
|
137
|
+
# try to find without the nested parameter
|
138
|
+
MyApi::Account.find(1)
|
139
|
+
=> raises ArgumentError
|
140
|
+
|
141
|
+
# makes request to /users/2/accounts/1
|
142
|
+
MyApi::Account.where(user_id: 2).find(1)
|
143
|
+
=> returns ResultSet
|
135
144
|
```
|
136
145
|
|
137
146
|
## Custom Methods
|
@@ -141,29 +150,112 @@ You can create custom methods on both collections (class method) and members (in
|
|
141
150
|
```
|
142
151
|
module MyApi
|
143
152
|
class User < JsonApiClient::Resource
|
144
|
-
|
145
|
-
# GET /users/search
|
153
|
+
|
154
|
+
# GET /users/search
|
146
155
|
custom_endpoint :search, on: :collection, request_method: :get
|
147
|
-
|
148
|
-
# PUT /users/:id/verify
|
156
|
+
|
157
|
+
# PUT /users/:id/verify
|
149
158
|
custom_endpoint :verify, on: :member, request_method: :put
|
150
159
|
end
|
151
160
|
end
|
161
|
+
|
162
|
+
# makes GET request to /users/search?name=Jeff
|
163
|
+
MyApi::User.search(name: 'Jeff')
|
164
|
+
=> <ResultSet of MyApi::User instances>
|
165
|
+
|
166
|
+
user = MyApi::User.find(1)
|
167
|
+
# makes PUT request to /users/1/verify?foo=bar
|
168
|
+
user.verify(foo: 'bar')
|
169
|
+
```
|
170
|
+
|
171
|
+
## Fetching Includes
|
172
|
+
|
173
|
+
[See specification](http://jsonapi.org/format/#fetching-includes)
|
174
|
+
|
175
|
+
If the response returns a [compound document](http://jsonapi.org/format/#document-structure-compound-documents), then we should be able to get the related resources.
|
176
|
+
|
177
|
+
```
|
178
|
+
# makes request to /articles/1?include=author,comments.author
|
179
|
+
results = Article.includes(:author, :comments => :author).find(1)
|
180
|
+
|
181
|
+
# should not have to make additional requests to the server
|
182
|
+
authors = results.map(&:author)
|
183
|
+
```
|
184
|
+
|
185
|
+
## Sparse Fieldsets
|
186
|
+
|
187
|
+
[See specification](http://jsonapi.org/format/#fetching-sparse-fieldsets)
|
188
|
+
|
189
|
+
```
|
190
|
+
# makes request to /articles?fields[articles]=title,body
|
191
|
+
article = Article.select("title,body").first
|
192
|
+
|
193
|
+
# should have fetched the requested fields
|
194
|
+
article.title
|
195
|
+
=> "Rails is Omakase"
|
196
|
+
|
197
|
+
# should not have returned the created_at
|
198
|
+
article.created_at
|
199
|
+
=> raise NoMethodError
|
152
200
|
```
|
153
201
|
|
154
|
-
|
202
|
+
## Sorting
|
203
|
+
|
204
|
+
[See specification](http://jsonapi.org/format/#fetching-sorting)
|
205
|
+
|
206
|
+
```
|
207
|
+
# makes request to /people?sort=+age
|
208
|
+
youngest = Person.sort(:age).all
|
209
|
+
|
210
|
+
# also makes request to /people?sort=+age
|
211
|
+
youngest = Person.sort(age: :asc).all
|
212
|
+
|
213
|
+
# makes request to /people?sort=-age
|
214
|
+
oldest = Person.sort(age: :desc).all
|
215
|
+
```
|
155
216
|
|
156
|
-
|
217
|
+
## Paginating
|
218
|
+
|
219
|
+
[See specification](http://jsonapi.org/format/#fetching-pagination)
|
220
|
+
|
221
|
+
### Requesting
|
222
|
+
|
223
|
+
```
|
224
|
+
# makes request to /articles?page=2&per_page=30
|
225
|
+
articles = Article.page(2).per(30).to_a
|
226
|
+
|
227
|
+
# also makes request to /articles?page=2&per_page=30
|
228
|
+
articles = Article.paginate(page: 2, per_page: 30).to_a
|
229
|
+
```
|
157
230
|
|
158
|
-
|
231
|
+
*Note: The mapping of pagination parameters is done by the `query_builder` which is [customizable](#fixme).*
|
159
232
|
|
160
|
-
|
233
|
+
### Browsing
|
161
234
|
|
162
|
-
|
235
|
+
If the response contains additional pagination links, you can also get at those:
|
236
|
+
|
237
|
+
```
|
238
|
+
articles = Article.paginate(page: 2, per_page: 30).to_a
|
239
|
+
articles.pages.next
|
240
|
+
articles.pages.last
|
241
|
+
```
|
242
|
+
|
243
|
+
### Library compatibility
|
244
|
+
|
245
|
+
A `JsonApiClient::ResultSet` object should be paginatable with both `kaminari` and `will_paginate`.
|
246
|
+
|
247
|
+
## Filtering
|
248
|
+
|
249
|
+
[See specifiation](http://jsonapi.org/format/#fetching-filtering)
|
250
|
+
|
251
|
+
```
|
252
|
+
# makes request to /people?filter[name]=Jeff
|
253
|
+
Person.where(name: 'Jeff').all
|
254
|
+
```
|
163
255
|
|
164
256
|
## Schema
|
165
257
|
|
166
|
-
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.
|
258
|
+
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.
|
167
259
|
|
168
260
|
The added benefit of declaring your schema is that you can access fields before data is set (otherwise, you'll get a `NoMethodError`).
|
169
261
|
|
@@ -200,6 +292,104 @@ The basic types that we allow are:
|
|
200
292
|
* `:int` or `:integer`
|
201
293
|
* `:float`
|
202
294
|
* `:string`
|
295
|
+
* `:time` - *Note: Include the time zone in the string if it's different than local time.
|
203
296
|
* `:boolean` - *Note: we will cast the string version of "true" and "false" to their respective values*
|
204
297
|
|
205
|
-
Also, we consider `nil` to be an acceptable value and will not cast the value.
|
298
|
+
Also, we consider `nil` to be an acceptable value and will not cast the value.
|
299
|
+
|
300
|
+
## Customizing
|
301
|
+
|
302
|
+
### Connections
|
303
|
+
|
304
|
+
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.
|
305
|
+
|
306
|
+
```
|
307
|
+
class NullConnection
|
308
|
+
def initialize(*args)
|
309
|
+
end
|
310
|
+
|
311
|
+
def run(request_method, path, params = {}, headers = {})
|
312
|
+
end
|
313
|
+
|
314
|
+
def use(*args); end
|
315
|
+
end
|
316
|
+
|
317
|
+
class CustomConnectionResource < TestResource
|
318
|
+
self.connection_class = NullConnection
|
319
|
+
end
|
320
|
+
|
321
|
+
```
|
322
|
+
|
323
|
+
#### Connection Options
|
324
|
+
|
325
|
+
You can configure your connection using Faraday middleware. In general, you'll want
|
326
|
+
to do this in a base model that all your resources inherit from:
|
327
|
+
|
328
|
+
```
|
329
|
+
MyApi::Base.connection do |connection|
|
330
|
+
# set OAuth2 headers
|
331
|
+
connection.use Faraday::Request::Oauth2, 'MYTOKEN'
|
332
|
+
|
333
|
+
# log responses
|
334
|
+
connection.use Faraday::Response::Logger
|
335
|
+
|
336
|
+
connection.use MyCustomMiddleware
|
337
|
+
end
|
338
|
+
|
339
|
+
module MyApi
|
340
|
+
class User < Base
|
341
|
+
# will use the customized connection
|
342
|
+
end
|
343
|
+
end
|
344
|
+
```
|
345
|
+
|
346
|
+
### Custom Parser
|
347
|
+
|
348
|
+
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:
|
349
|
+
|
350
|
+
```
|
351
|
+
class MyCustomParser
|
352
|
+
def self.parse(klass, response)
|
353
|
+
…
|
354
|
+
# returns some ResultSet object
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
class MyApi::Base < JsonApiClient::Resource
|
359
|
+
self.parser = MyCustomParser
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
### Custom Query Builder
|
364
|
+
|
365
|
+
You can customize how the scope builder methods map to request parameters.
|
366
|
+
|
367
|
+
```
|
368
|
+
class MyQueryBuilder
|
369
|
+
def def initialize(klass); end
|
370
|
+
|
371
|
+
def where(conditions = {})
|
372
|
+
end
|
373
|
+
|
374
|
+
… add order, includes, paginate, page, first, build
|
375
|
+
end
|
376
|
+
|
377
|
+
class MyApi::Base < JsonApiClient::Resource
|
378
|
+
self.query_builder = MyQueryBuilder
|
379
|
+
end
|
380
|
+
```
|
381
|
+
|
382
|
+
### Custom Paginator
|
383
|
+
|
384
|
+
You can customize how your resources find pagination information from the response.
|
385
|
+
|
386
|
+
```
|
387
|
+
class MyPaginator
|
388
|
+
def initialize(result_set, data); end
|
389
|
+
# implement current_page, total_entries, etc
|
390
|
+
end
|
391
|
+
|
392
|
+
class MyApi::Base < JsonApiClient::Resource
|
393
|
+
self.paginator = MyPaginator
|
394
|
+
end
|
395
|
+
```
|
data/lib/json_api_client.rb
CHANGED
@@ -1,20 +1,25 @@
|
|
1
1
|
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
2
3
|
require 'json'
|
4
|
+
require "addressable/uri"
|
3
5
|
|
4
6
|
module JsonApiClient
|
5
7
|
autoload :Associations, 'json_api_client/associations'
|
6
8
|
autoload :Attributes, 'json_api_client/attributes'
|
7
9
|
autoload :Connection, 'json_api_client/connection'
|
8
10
|
autoload :Errors, 'json_api_client/errors'
|
11
|
+
autoload :ErrorCollector, 'json_api_client/error_collector'
|
9
12
|
autoload :Helpers, 'json_api_client/helpers'
|
13
|
+
autoload :IncludedData, 'json_api_client/included_data'
|
14
|
+
autoload :Linking, 'json_api_client/linking'
|
10
15
|
autoload :LinkDefinition, 'json_api_client/link_definition'
|
11
|
-
autoload :
|
16
|
+
autoload :MetaData, 'json_api_client/meta_data'
|
12
17
|
autoload :Middleware, 'json_api_client/middleware'
|
13
|
-
autoload :
|
18
|
+
autoload :Paginating, 'json_api_client/paginating'
|
19
|
+
autoload :Parsers, 'json_api_client/parsers'
|
14
20
|
autoload :Query, 'json_api_client/query'
|
15
21
|
autoload :Resource, 'json_api_client/resource'
|
16
22
|
autoload :ResultSet, 'json_api_client/result_set'
|
17
23
|
autoload :Schema, 'json_api_client/schema'
|
18
|
-
autoload :Scope, 'json_api_client/scope'
|
19
24
|
autoload :Utils, 'json_api_client/utils'
|
20
|
-
end
|
25
|
+
end
|