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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +274 -84
  3. data/lib/json_api_client.rb +9 -4
  4. data/lib/json_api_client/connection.rb +5 -5
  5. data/lib/json_api_client/error_collector.rb +29 -0
  6. data/lib/json_api_client/errors.rb +7 -5
  7. data/lib/json_api_client/helpers.rb +3 -0
  8. data/lib/json_api_client/helpers/associable.rb +4 -3
  9. data/lib/json_api_client/helpers/attributable.rb +15 -46
  10. data/lib/json_api_client/helpers/custom_endpoints.rb +3 -10
  11. data/lib/json_api_client/helpers/dynamic_attributes.rb +61 -0
  12. data/lib/json_api_client/helpers/linkable.rb +28 -18
  13. data/lib/json_api_client/helpers/paginatable.rb +13 -0
  14. data/lib/json_api_client/helpers/parsable.rb +1 -1
  15. data/lib/json_api_client/helpers/queryable.rb +4 -3
  16. data/lib/json_api_client/helpers/requestable.rb +60 -0
  17. data/lib/json_api_client/linking.rb +7 -0
  18. data/lib/json_api_client/linking/included_data.rb +40 -0
  19. data/lib/json_api_client/linking/links.rb +29 -0
  20. data/lib/json_api_client/linking/top_level_links.rb +30 -0
  21. data/lib/json_api_client/meta_data.rb +10 -0
  22. data/lib/json_api_client/middleware/json_request.rb +3 -4
  23. data/lib/json_api_client/middleware/status.rb +10 -1
  24. data/lib/json_api_client/paginating.rb +5 -0
  25. data/lib/json_api_client/paginating/paginator.rb +80 -0
  26. data/lib/json_api_client/parsers.rb +5 -0
  27. data/lib/json_api_client/parsers/parser.rb +55 -0
  28. data/lib/json_api_client/query.rb +2 -7
  29. data/lib/json_api_client/query/builder.rb +126 -0
  30. data/lib/json_api_client/query/requestor.rb +77 -0
  31. data/lib/json_api_client/resource.rb +3 -59
  32. data/lib/json_api_client/result_set.rb +11 -29
  33. data/lib/json_api_client/schema.rb +15 -30
  34. data/lib/json_api_client/version.rb +1 -1
  35. metadata +36 -19
  36. data/lib/json_api_client/link.rb +0 -11
  37. data/lib/json_api_client/link_definition.rb +0 -27
  38. data/lib/json_api_client/linked_data.rb +0 -75
  39. data/lib/json_api_client/parser.rb +0 -63
  40. data/lib/json_api_client/query/base.rb +0 -38
  41. data/lib/json_api_client/query/create.rb +0 -17
  42. data/lib/json_api_client/query/custom.rb +0 -22
  43. data/lib/json_api_client/query/destroy.rb +0 -12
  44. data/lib/json_api_client/query/find.rb +0 -19
  45. data/lib/json_api_client/query/linked.rb +0 -24
  46. data/lib/json_api_client/query/update.rb +0 -13
  47. data/lib/json_api_client/scope.rb +0 -48
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 574b5a997ebfb62e31f64d3ff344e4c35d1ef5a9
4
- data.tar.gz: 018500b78e06fc27c2d4edb577fede4d698b4242
3
+ metadata.gz: cab5bf2208be347ffc15b03c0af11c4c21ace976
4
+ data.tar.gz: 98074a5fcacd34fb0a4dc946f8968957437b8f3a
5
5
  SHA512:
6
- metadata.gz: 6b844103832648f10661d8f5f5b73ffd261e5520be262bddd1b721c82dd0cc85b4661bebaa4b6364d3024ffacfec64755d0f75109517a28c6c85d539a0887304
7
- data.tar.gz: d00a5471d7851a11455ce66b32d7595eb9284fefa1c3a398599022eed5928e9c8aaaa190965add826ca66a35a4270cea740e87d788e03eaa150f891ff2ec9c5d
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 User < JsonApiClient::Resource
12
- has_many :accounts
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 Account < JsonApiClient::Resource
16
- belongs_to :user
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
- MyApi::User.all
21
- MyApi::User.where(account_id: 1).find(1)
22
- MyApi::User.where(account_id: 1).all
34
+ Some example usage:
23
35
 
24
- MyApi::User.where(name: "foo").order("created_at desc").includes(:preferences, :cars).all
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
- u = MyApi::User.new(foo: "bar", bar: "foo")
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::User.find(1).first
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::User.create(
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
- ## Connection Options
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.json
153
+
154
+ # GET /users/search
146
155
  custom_endpoint :search, on: :collection, request_method: :get
147
-
148
- # PUT /users/:id/verify.json
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
- In the above scenario, you can call the class method `MyApi::User.search`. The results will be parsed like any other query. If the response returns users, you will get back a `ResultSet` of `MyApi::User` instances.
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
- You can also call the instance method `verify` on a `MyApi::User` instance.
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
- ## Links
231
+ *Note: The mapping of pagination parameters is done by the `query_builder` which is [customizable](#fixme).*
159
232
 
160
- We also respect the [links specification](http://jsonapi.org/format/#document-structure-resource-relationships). The client can fetch linked resources based on the defined endpoint from the link specification as well as load data from any `linked` data provided in the response. Additionally, it will still fetch missing data if not all linked resources are provided in the `linked` data response.
233
+ ### Browsing
161
234
 
162
- See the [tests](https://github.com/chingor13/json_api_client/blob/master/test/unit/links_test.rb).
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
+ ```
@@ -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 :LinkedData, 'json_api_client/linked_data'
16
+ autoload :MetaData, 'json_api_client/meta_data'
12
17
  autoload :Middleware, 'json_api_client/middleware'
13
- autoload :Parser, 'json_api_client/parser'
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