jsonapi-resources 0.8.3 → 0.9.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2124 -8
- data/lib/jsonapi-resources.rb +2 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +70 -29
- data/lib/jsonapi/cached_resource_fragment.rb +119 -0
- data/lib/jsonapi/compiled_json.rb +36 -0
- data/lib/jsonapi/configuration.rb +54 -4
- data/lib/jsonapi/error_codes.rb +2 -2
- data/lib/jsonapi/exceptions.rb +19 -13
- data/lib/jsonapi/formatter.rb +15 -1
- data/lib/jsonapi/include_directives.rb +23 -3
- data/lib/jsonapi/processor.rb +69 -27
- data/lib/jsonapi/relationship_builder.rb +23 -21
- data/lib/jsonapi/request_parser.rb +27 -72
- data/lib/jsonapi/resource.rb +234 -38
- data/lib/jsonapi/resource_serializer.rb +229 -95
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +9 -20
- metadata +25 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6215487122618a6e8526cb39da9ce37f494f248
|
4
|
+
data.tar.gz: d8cb24a422186e9dde1488dc172c072cbc456068
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b831258524a83611c492b61fdce91d5b6744b7ef788e038cad3c69955142992d8cfaf156f4cb907a07968dd6b7a0789fabf8d6d0227410d66ddd6867d99bd77
|
7
|
+
data.tar.gz: ca5168dd8dd8de04a67ef6ae63160b26e4e0c2e863bc591261b0091063286bfe292ee35eedcaa250dd5637c2fd3eb9e7e5d15241aff17fe05fe15ca34dfbee0b
|
data/README.md
CHANGED
@@ -2,18 +2,60 @@
|
|
2
2
|
|
3
3
|
[![Join the chat at https://gitter.im/cerebris/jsonapi-resources](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cerebris/jsonapi-resources?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
4
|
|
5
|
-
|
6
|
-
[
|
5
|
+
**NOTE:** This README is the documentation for `JSONAPI::Resources`. If you are viewing this at the
|
6
|
+
[project page on Github](https://github.com/cerebris/jsonapi-resources) you are viewing the documentation for the `master`
|
7
|
+
branch. This may contain information that is not relevant to the release you are using. Please see the README for the
|
8
|
+
[version](https://github.com/cerebris/jsonapi-resources/releases) you are using.
|
7
9
|
|
8
|
-
|
10
|
+
---
|
11
|
+
|
12
|
+
`JSONAPI::Resources`, or "JR", provides a framework for developing a server that complies with the
|
13
|
+
[JSON API](http://jsonapi.org/) specification.
|
14
|
+
|
15
|
+
Like JSON API itself, JR's design is focused on the resources served by an API. JR needs little more than a definition
|
9
16
|
of your resources, including their attributes and relationships, to make your server compliant with JSON API.
|
10
17
|
|
11
18
|
JR is designed to work with Rails 4.0+, and provides custom routes, controllers, and serializers. JR's resources may be
|
12
19
|
backed by ActiveRecord models or by custom objects.
|
13
20
|
|
14
|
-
##
|
21
|
+
## Table of Contents
|
15
22
|
|
16
|
-
|
23
|
+
* [Demo App] (#demo-app)
|
24
|
+
* [Client Libraries] (#client-libraries)
|
25
|
+
* [Installation] (#installation)
|
26
|
+
* [Usage] (#usage)
|
27
|
+
* [Resources] (#resources)
|
28
|
+
* [JSONAPI::Resource] (#jsonapiresource)
|
29
|
+
* [Context] (#context)
|
30
|
+
* [Attributes] (#attributes)
|
31
|
+
* [Primary Key] (#primary-key)
|
32
|
+
* [Model Name] (#model-name)
|
33
|
+
* [Model Hints] (#model-hints)
|
34
|
+
* [Relationships] (#relationships)
|
35
|
+
* [Filters] (#filters)
|
36
|
+
* [Pagination] (#pagination)
|
37
|
+
* [Included relationships (side-loading resources)] (#included-relationships-side-loading-resources)
|
38
|
+
* [Resource meta] (#resource-meta)
|
39
|
+
* [Custom Links] (#custom-links)
|
40
|
+
* [Callbacks] (#callbacks)
|
41
|
+
* [Controllers] (#controllers)
|
42
|
+
* [Namespaces] (#namespaces)
|
43
|
+
* [Error Codes] (#error-codes)
|
44
|
+
* [Handling Exceptions] (#handling-exceptions)
|
45
|
+
* [Action Callbacks] (#action-callbacks)
|
46
|
+
* [Operation Processors] (#operation-processors)
|
47
|
+
* [Serializer] (#serializer)
|
48
|
+
* [Serializer options] (#serializer-options)
|
49
|
+
* [Formatting] (#formatting)
|
50
|
+
* [Key Format] (#key-format)
|
51
|
+
* [Routing] (#routing)
|
52
|
+
* [Nested Routes] (#nested-routes)
|
53
|
+
* [Authorization](#authorization)
|
54
|
+
* [Resource Caching] (#resource-caching)
|
55
|
+
* [Caching Caveats] (#caching-caveats)
|
56
|
+
* [Configuration] (#configuration)
|
57
|
+
* [Contributing] (#contributing)
|
58
|
+
* [License] (#license)
|
17
59
|
|
18
60
|
## Demo App
|
19
61
|
|
@@ -21,8 +63,8 @@ We have a simple demo app, called [Peeps](https://github.com/cerebris/peeps), av
|
|
21
63
|
|
22
64
|
## Client Libraries
|
23
65
|
|
24
|
-
JSON
|
25
|
-
which *should* be compatible with JSON
|
66
|
+
JSON API maintains a (non-verified) listing of [client libraries](http://jsonapi.org/implementations/#client-libraries)
|
67
|
+
which *should* be compatible with JSON API compliant server implementations such as JR.
|
26
68
|
|
27
69
|
## Installation
|
28
70
|
|
@@ -38,7 +80,2071 @@ Or install it yourself as:
|
|
38
80
|
|
39
81
|
$ gem install jsonapi-resources
|
40
82
|
|
41
|
-
|
83
|
+
## Usage
|
84
|
+
|
85
|
+
### Resources
|
86
|
+
|
87
|
+
Resources define the public interface to your API. A resource defines which attributes are exposed, as well as
|
88
|
+
relationships to other resources.
|
89
|
+
|
90
|
+
Resource definitions should by convention be placed in a directory under app named resources, `app/resources`. The file name should be the single underscored name of the model that backs the resource with `_resource.rb` appended. For example,
|
91
|
+
a `Contact` model's resource should have a class named `ContactResource` defined in a file named `contact_resource.rb`.
|
92
|
+
|
93
|
+
#### JSONAPI::Resource
|
94
|
+
|
95
|
+
Resources must be derived from `JSONAPI::Resource`, or a class that is itself derived from `JSONAPI::Resource`.
|
96
|
+
|
97
|
+
For example:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class ContactResource < JSONAPI::Resource
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
A jsonapi-resource generator is available
|
105
|
+
```
|
106
|
+
rails generate jsonapi:resource contact
|
107
|
+
```
|
108
|
+
|
109
|
+
##### Abstract Resources
|
110
|
+
|
111
|
+
Resources that are not backed by a model (purely used as base classes for other resources) should be declared as
|
112
|
+
abstract.
|
113
|
+
|
114
|
+
Because abstract resources do not expect to be backed by a model, they won't attempt to discover the model class
|
115
|
+
or any of its relationships.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class BaseResource < JSONAPI::Resource
|
119
|
+
abstract
|
120
|
+
|
121
|
+
has_one :creator
|
122
|
+
end
|
123
|
+
|
124
|
+
class ContactResource < BaseResource
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
##### Immutable Resources
|
129
|
+
|
130
|
+
Resources that are immutable should be declared as such with the `immutable` method. Immutable resources will only
|
131
|
+
generate routes for `index`, `show` and `show_relationship`.
|
132
|
+
|
133
|
+
###### Immutable for Readonly
|
134
|
+
|
135
|
+
Some resources are read-only and are not to be modified through the API. Declaring a resource as immutable prevents
|
136
|
+
creation of routes that allow modification of the resource.
|
137
|
+
|
138
|
+
###### Immutable Heterogeneous Collections
|
139
|
+
|
140
|
+
Immutable resources can be used as the basis for a heterogeneous collection. Resources in heterogeneous collections can
|
141
|
+
still be mutated through their own type-specific endpoints.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class VehicleResource < JSONAPI::Resource
|
145
|
+
immutable
|
146
|
+
|
147
|
+
has_one :owner
|
148
|
+
attributes :make, :model, :serial_number
|
149
|
+
end
|
150
|
+
|
151
|
+
class CarResource < VehicleResource
|
152
|
+
attributes :drive_layout
|
153
|
+
has_one :driver
|
154
|
+
end
|
155
|
+
|
156
|
+
class BoatResource < VehicleResource
|
157
|
+
attributes :length_at_water_line
|
158
|
+
has_one :captain
|
159
|
+
end
|
160
|
+
|
161
|
+
# routes
|
162
|
+
jsonapi_resources :vehicles
|
163
|
+
jsonapi_resources :cars
|
164
|
+
jsonapi_resources :boats
|
165
|
+
|
166
|
+
```
|
167
|
+
|
168
|
+
In the above example vehicles are immutable. A call to `/vehicles` or `/vehicles/1` will return vehicles with types
|
169
|
+
of either `car` or `boat`. But calls to PUT or POST a `car` must be made to `/cars`. The rails models backing the above
|
170
|
+
code use Single Table Inheritance.
|
171
|
+
|
172
|
+
#### Context
|
173
|
+
|
174
|
+
Sometimes you will want to access things such as the current logged in user (and other state only available within your controllers) from within your resource classes. To make this state available to a resource class you need to put it into the context hash - this can be done via a `context` method on one of your controllers or across all controllers using ApplicationController.
|
175
|
+
|
176
|
+
For example:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class ApplicationController < JSONAPI::ResourceController
|
180
|
+
def context
|
181
|
+
{current_user: current_user}
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Specific resource controllers derive from ApplicationController
|
186
|
+
# and share its context
|
187
|
+
class PeopleController < ApplicationController
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
# Assuming you don't permit user_id (so the client won't assign a wrong user to own the object)
|
192
|
+
# you can ensure the current user is assigned the record by using the controller's context hash.
|
193
|
+
class PeopleResource < JSONAPI::Resource
|
194
|
+
before_save do
|
195
|
+
@model.user_id = context[:current_user].id if @model.new_record?
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
You can put things that affect serialization and resource configuration into the context.
|
201
|
+
|
202
|
+
#### Attributes
|
203
|
+
|
204
|
+
Any of a resource's attributes that are accessible must be explicitly declared. Single attributes can be declared using
|
205
|
+
the `attribute` method, and multiple attributes can be declared with the `attributes` method on the resource class.
|
206
|
+
|
207
|
+
For example:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
class ContactResource < JSONAPI::Resource
|
211
|
+
attribute :name_first
|
212
|
+
attributes :name_last, :email, :twitter
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
This resource has 4 defined attributes: `name_first`, `name_last`, `email`, `twitter`, as well as the automatically
|
217
|
+
defined attributes `id` and `type`. By default these attributes must exist on the model that is handled by the resource.
|
218
|
+
|
219
|
+
A resource object wraps a Ruby object, usually an `ActiveModel` record, which is available as the `@model` variable.
|
220
|
+
This allows a resource's methods to access the underlying model.
|
221
|
+
|
222
|
+
For example, a computed attribute for `full_name` could be defined as such:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class ContactResource < JSONAPI::Resource
|
226
|
+
attributes :name_first, :name_last, :email, :twitter
|
227
|
+
attribute :full_name
|
228
|
+
|
229
|
+
def full_name
|
230
|
+
"#{@model.name_first}, #{@model.name_last}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
##### Attribute Delegation
|
236
|
+
|
237
|
+
Normally resource attributes map to an attribute on the model of the same name. Using the `delegate` option allows a resource
|
238
|
+
attribute to map to a differently named model attribute. For example:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
class ContactResource < JSONAPI::Resource
|
242
|
+
attribute :name_first, delegate: :first_name
|
243
|
+
attribute :name_last, delegate: :last_name
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
##### Fetchable Attributes
|
248
|
+
|
249
|
+
By default all attributes are assumed to be fetchable. The list of fetchable attributes can be filtered by overriding
|
250
|
+
the `fetchable_fields` method.
|
251
|
+
|
252
|
+
Here's an example that prevents guest users from seeing the `email` field:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
class AuthorResource < JSONAPI::Resource
|
256
|
+
attributes :name, :email
|
257
|
+
model_name 'Person'
|
258
|
+
has_many :posts
|
259
|
+
|
260
|
+
def fetchable_fields
|
261
|
+
if (context[:current_user].guest)
|
262
|
+
super - [:email]
|
263
|
+
else
|
264
|
+
super
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
Context flows through from the controller to the resource and can be used to control the attributes based on the
|
271
|
+
current user (or other value).
|
272
|
+
|
273
|
+
##### Creatable and Updatable Attributes
|
274
|
+
|
275
|
+
By default all attributes are assumed to be updatable and creatable. To prevent some attributes from being accepted by
|
276
|
+
the `update` or `create` methods, override the `self.updatable_fields` and `self.creatable_fields` methods on a resource.
|
277
|
+
|
278
|
+
This example prevents `full_name` from being set:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
class ContactResource < JSONAPI::Resource
|
282
|
+
attributes :name_first, :name_last, :full_name
|
283
|
+
|
284
|
+
def full_name
|
285
|
+
"#{@model.name_first}, #{@model.name_last}"
|
286
|
+
end
|
287
|
+
|
288
|
+
def self.updatable_fields(context)
|
289
|
+
super - [:full_name]
|
290
|
+
end
|
291
|
+
|
292
|
+
def self.creatable_fields(context)
|
293
|
+
super - [:full_name]
|
294
|
+
end
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
298
|
+
The `context` is not by default used by the `ResourceController`, but may be used if you override the controller methods.
|
299
|
+
By using the context you have the option to determine the creatable and updatable fields based on the user.
|
300
|
+
|
301
|
+
##### Sortable Attributes
|
302
|
+
|
303
|
+
JR supports [sorting primary resources by multiple sort criteria](http://jsonapi.org/format/#fetching-sorting).
|
304
|
+
|
305
|
+
By default all attributes are assumed to be sortable. To prevent some attributes from being sortable, override the
|
306
|
+
`self.sortable_fields` method on a resource.
|
307
|
+
|
308
|
+
Here's an example that prevents sorting by post's `body`:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
class PostResource < JSONAPI::Resource
|
312
|
+
attributes :title, :body
|
313
|
+
|
314
|
+
def self.sortable_fields(context)
|
315
|
+
super(context) - [:body]
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
JR also supports sorting primary resources by fields on relationships.
|
321
|
+
|
322
|
+
Here's an example of sorting books by the author name:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
class Book < ActiveRecord::Base
|
326
|
+
belongs_to :author
|
327
|
+
end
|
328
|
+
|
329
|
+
class Author < ActiveRecord::Base
|
330
|
+
has_many :books
|
331
|
+
end
|
332
|
+
|
333
|
+
class BookResource < JSONAPI::Resource
|
334
|
+
attributes :title, :body
|
335
|
+
|
336
|
+
def self.sortable_fields(context)
|
337
|
+
super(context) << :"author.name"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
```
|
341
|
+
The request will look something like:
|
342
|
+
```
|
343
|
+
GET /books?include=author&sort=author.name
|
344
|
+
```
|
345
|
+
|
346
|
+
###### Default sorting
|
347
|
+
|
348
|
+
By default JR sorts ascending on the `id` of the primary resource, unless the request specifies an alternate sort order.
|
349
|
+
To override this you may override the `self.default_sort` on a `resource`. `default_sort` should return an array of
|
350
|
+
`sort_param` hashes. A `sort_param` hash contains a `field` and a `direction`, with `direction` being either `:asc` or
|
351
|
+
`:desc`.
|
352
|
+
|
353
|
+
For example:
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
def self.default_sort
|
357
|
+
[{field: 'name_last', direction: :desc}, {field: 'name_first', direction: :desc}]
|
358
|
+
end
|
359
|
+
```
|
360
|
+
|
361
|
+
##### Attribute Formatting
|
362
|
+
|
363
|
+
Attributes can have a `Format`. By default all attributes use the default formatter. If an attribute has the `format`
|
364
|
+
option set the system will attempt to find a formatter based on this name. In the following example the `last_login_time`
|
365
|
+
will be returned formatted to a certain time zone:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
class PersonResource < JSONAPI::Resource
|
369
|
+
attributes :name, :email
|
370
|
+
attribute :last_login_time, format: :date_with_timezone
|
371
|
+
end
|
372
|
+
```
|
373
|
+
|
374
|
+
The system will lookup a value formatter named `DateWithTimezoneValueFormatter` and will use this when serializing and
|
375
|
+
updating the attribute. See the [Value Formatters](#value-formatters) section for more details.
|
376
|
+
|
377
|
+
##### Flattening a Rails relationship
|
378
|
+
|
379
|
+
It is possible to flatten Rails relationships into attributes by using getters and setters. This can become handy if a relation needs to be created alongside the creation of the main object which can be the case if there is a bi-directional presence validation. For example:
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
# Given Models
|
383
|
+
class Person < ActiveRecord::Base
|
384
|
+
has_many :spoken_languages
|
385
|
+
validates :name, :email, :spoken_languages, presence: true
|
386
|
+
end
|
387
|
+
|
388
|
+
class SpokenLanguage < ActiveRecord::Base
|
389
|
+
belongs_to :person, inverse_of: :spoken_languages
|
390
|
+
validates :person, :language_code, presence: true
|
391
|
+
end
|
392
|
+
|
393
|
+
# Resource with getters and setter
|
394
|
+
class PersonResource < JSONAPI::Resource
|
395
|
+
attributes :name, :email, :spoken_languages
|
396
|
+
|
397
|
+
# Getter
|
398
|
+
def spoken_languages
|
399
|
+
@model.spoken_languages.pluck(:language_code)
|
400
|
+
end
|
401
|
+
|
402
|
+
# Setter (because spoken_languages needed for creation)
|
403
|
+
def spoken_languages=(new_spoken_language_codes)
|
404
|
+
@model.spoken_languages.destroy_all
|
405
|
+
new_spoken_language_codes.each do |new_lang_code|
|
406
|
+
@model.spoken_languages.build(language_code: new_lang_code)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
```
|
411
|
+
|
412
|
+
#### Primary Key
|
413
|
+
|
414
|
+
Resources are always represented using a key of `id`. The resource will interrogate the model to find the primary key.
|
415
|
+
If the underlying model does not use `id` as the primary key _and_ does not support the `primary_key` method you
|
416
|
+
must use the `primary_key` method to tell the resource which field on the model to use as the primary key. **Note:**
|
417
|
+
this _must_ be the actual primary key of the model.
|
418
|
+
|
419
|
+
By default only integer values are allowed for primary key. To change this behavior you can set the `resource_key_type`
|
420
|
+
configuration option:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
JSONAPI.configure do |config|
|
424
|
+
# Allowed values are :integer(default), :uuid, :string, or a proc
|
425
|
+
config.resource_key_type = :uuid
|
426
|
+
end
|
427
|
+
```
|
428
|
+
|
429
|
+
##### Override key type on a resource
|
430
|
+
|
431
|
+
You can override the default resource key type on a per-resource basis by calling `key_type` in the resource class,
|
432
|
+
with the same allowed values as the `resource_key_type` configuration option.
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
class ContactResource < JSONAPI::Resource
|
436
|
+
attribute :id
|
437
|
+
attributes :name_first, :name_last, :email, :twitter
|
438
|
+
key_type :uuid
|
439
|
+
end
|
440
|
+
```
|
441
|
+
|
442
|
+
##### Custom resource key validators
|
443
|
+
|
444
|
+
If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that
|
445
|
+
accepts key and context arguments in `config/initializers/jsonapi_resources.rb`:
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
JSONAPI.configure do |config|
|
449
|
+
config.resource_key_type = -> (key, context) { key && String(key) }
|
450
|
+
end
|
451
|
+
```
|
452
|
+
|
453
|
+
#### Model Name
|
454
|
+
|
455
|
+
The name of the underlying model is inferred from the Resource name. It can be overridden by use of the `model_name`
|
456
|
+
method. For example:
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
class AuthorResource < JSONAPI::Resource
|
460
|
+
attribute :name
|
461
|
+
model_name 'Person'
|
462
|
+
has_many :posts
|
463
|
+
end
|
464
|
+
```
|
465
|
+
|
466
|
+
#### Model Hints
|
467
|
+
|
468
|
+
Resource instances are created from model records. The determination of the correct resource type is performed using a
|
469
|
+
simple rule based on the model's name. The name is used to find a resource in the same module (as the originating
|
470
|
+
resource) that matches the name. This usually works quite well, however it can fail when model names do not match
|
471
|
+
resource names. It can also fail when using namespaced models. In this case a `model_hint` can be created to map model
|
472
|
+
names to resources. For example:
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
class AuthorResource < JSONAPI::Resource
|
476
|
+
attribute :name
|
477
|
+
model_name 'Person'
|
478
|
+
model_hint model: Commenter, resource: :special_person
|
479
|
+
|
480
|
+
has_many :posts
|
481
|
+
has_many :commenters
|
482
|
+
end
|
483
|
+
```
|
484
|
+
|
485
|
+
Note that when `model_name` is set a corresponding `model_hint` is also added. This can be skipped by using the
|
486
|
+
`add_model_hint` option set to false. For example:
|
487
|
+
|
488
|
+
```ruby
|
489
|
+
class AuthorResource < JSONAPI::Resource
|
490
|
+
model_name 'Legacy::Person', add_model_hint: false
|
491
|
+
end
|
492
|
+
```
|
493
|
+
|
494
|
+
Model hints inherit from parent resources, but are not global in scope. The `model_hint` method accepts `model` and
|
495
|
+
`resource` named parameters. `model` takes an ActiveRecord class or class name (defaults to the model name), and
|
496
|
+
`resource` takes a resource type or a resource class (defaults to the current resource's type).
|
497
|
+
|
498
|
+
#### Relationships
|
499
|
+
|
500
|
+
Related resources need to be specified in the resource. These may be declared with the `relationship` or the `has_one`
|
501
|
+
and the `has_many` methods.
|
502
|
+
|
503
|
+
Here's a simple example using the `relationship` method where a post has a single author and an author can have many
|
504
|
+
posts:
|
505
|
+
|
506
|
+
```ruby
|
507
|
+
class PostResource < JSONAPI::Resource
|
508
|
+
attributes :title, :body
|
509
|
+
|
510
|
+
relationship :author, to: :one
|
511
|
+
end
|
512
|
+
```
|
513
|
+
|
514
|
+
And the corresponding author:
|
515
|
+
|
516
|
+
```ruby
|
517
|
+
class AuthorResource < JSONAPI::Resource
|
518
|
+
attribute :name
|
519
|
+
|
520
|
+
relationship :posts, to: :many
|
521
|
+
end
|
522
|
+
```
|
523
|
+
|
524
|
+
And here's the equivalent resources using the `has_one` and `has_many` methods:
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
class PostResource < JSONAPI::Resource
|
528
|
+
attributes :title, :body
|
529
|
+
|
530
|
+
has_one :author
|
531
|
+
end
|
532
|
+
```
|
533
|
+
|
534
|
+
And the corresponding author:
|
535
|
+
|
536
|
+
```ruby
|
537
|
+
class AuthorResource < JSONAPI::Resource
|
538
|
+
attribute :name
|
539
|
+
|
540
|
+
has_many :posts
|
541
|
+
end
|
542
|
+
```
|
543
|
+
|
544
|
+
##### Options
|
545
|
+
|
546
|
+
The relationship methods (`relationship`, `has_one`, and `has_many`) support the following options:
|
547
|
+
|
548
|
+
* `class_name` - a string specifying the underlying class for the related resource. Defaults to the `class_name` property on the underlying model.
|
549
|
+
* `foreign_key` - the method on the resource used to fetch the related resource. Defaults to `<resource_name>_id` for has_one and `<resource_name>_ids` for has_many relationships.
|
550
|
+
* `acts_as_set` - allows the entire set of related records to be replaced in one operation. Defaults to false if not set.
|
551
|
+
* `polymorphic` - set to true to identify relationships that are polymorphic.
|
552
|
+
* `relation_name` - the name of the relation to use on the model. A lambda may be provided which allows conditional selection of the relation based on the context.
|
553
|
+
* `always_include_linkage_data` - if set to true, the relationship includes linkage data. Defaults to false if not set.
|
554
|
+
* `eager_load_on_include` - if set to false, will not include this relationship in join SQL when requested via an include. You usually want to leave this on, but it will break 'relationships' which are not active record, for example if you want to expose a tree using the `ancestry` gem or similar, or the SQL query becomes too large to handle. Defaults to true if not set.
|
555
|
+
|
556
|
+
`to_one` relationships support the additional option:
|
557
|
+
* `foreign_key_on` - defaults to `:self`. To indicate that the foreign key is on the related resource specify `:related`.
|
558
|
+
|
559
|
+
`to_many` relationships support the additional option:
|
560
|
+
* `reflect` - defaults to `true`. To indicate that updates to the relationship are performed on the related resource, if relationship reflection is turned on. See [Configuration] (#configuration)
|
561
|
+
|
562
|
+
Examples:
|
563
|
+
|
564
|
+
```ruby
|
565
|
+
class CommentResource < JSONAPI::Resource
|
566
|
+
attributes :body
|
567
|
+
has_one :post
|
568
|
+
has_one :author, class_name: 'Person'
|
569
|
+
has_many :tags, acts_as_set: true
|
570
|
+
end
|
571
|
+
|
572
|
+
class ExpenseEntryResource < JSONAPI::Resource
|
573
|
+
attributes :cost, :transaction_date
|
574
|
+
|
575
|
+
has_one :currency, class_name: 'Currency', foreign_key: 'currency_code'
|
576
|
+
has_one :employee
|
577
|
+
end
|
578
|
+
|
579
|
+
class TagResource < JSONAPI::Resource
|
580
|
+
attributes :name
|
581
|
+
has_one :taggable, polymorphic: true
|
582
|
+
end
|
583
|
+
```
|
584
|
+
|
585
|
+
```ruby
|
586
|
+
class BookResource < JSONAPI::Resource
|
587
|
+
|
588
|
+
# Only book_admins may see unapproved comments for a book. Using
|
589
|
+
# a lambda to select the correct relation on the model
|
590
|
+
has_many :book_comments, relation_name: -> (options = {}) {
|
591
|
+
context = options[:context]
|
592
|
+
current_user = context ? context[:current_user] : nil
|
593
|
+
|
594
|
+
unless current_user && current_user.book_admin
|
595
|
+
:approved_book_comments
|
596
|
+
else
|
597
|
+
:book_comments
|
598
|
+
end
|
599
|
+
}
|
600
|
+
...
|
601
|
+
end
|
602
|
+
```
|
603
|
+
|
604
|
+
The polymorphic relationship will require the resource and controller to exist, although routing to them will cause an
|
605
|
+
error.
|
606
|
+
|
607
|
+
```ruby
|
608
|
+
class TaggableResource < JSONAPI::Resource; end
|
609
|
+
class TaggablesController < JSONAPI::ResourceController; end
|
610
|
+
```
|
611
|
+
|
612
|
+
#### Filters
|
613
|
+
|
614
|
+
Filters for locating objects of the resource type are specified in the resource definition. Single filters can be
|
615
|
+
declared using the `filter` method, and multiple filters can be declared with the `filters` method on the resource
|
616
|
+
class.
|
617
|
+
|
618
|
+
For example:
|
619
|
+
|
620
|
+
```ruby
|
621
|
+
class ContactResource < JSONAPI::Resource
|
622
|
+
attributes :name_first, :name_last, :email, :twitter
|
623
|
+
|
624
|
+
filter :id
|
625
|
+
filters :name_first, :name_last
|
626
|
+
end
|
627
|
+
```
|
628
|
+
|
629
|
+
Then a request could pass in a filter for example `http://example.com/contacts?filter[name_last]=Smith` and the system
|
630
|
+
will find all people where the last name exactly matches Smith.
|
631
|
+
|
632
|
+
##### Default Filters
|
633
|
+
|
634
|
+
A default filter may be defined for a resource using the `default` option on the `filter` method. This default is used
|
635
|
+
unless the request overrides this value.
|
636
|
+
|
637
|
+
For example:
|
638
|
+
|
639
|
+
```ruby
|
640
|
+
class CommentResource < JSONAPI::Resource
|
641
|
+
attributes :body, :status
|
642
|
+
has_one :post
|
643
|
+
has_one :author
|
644
|
+
|
645
|
+
filter :status, default: 'published,pending'
|
646
|
+
end
|
647
|
+
```
|
648
|
+
|
649
|
+
The default value is used as if it came from the request.
|
650
|
+
|
651
|
+
##### Applying Filters
|
652
|
+
|
653
|
+
You may customize how a filter behaves by supplying a callable to the `:apply` option. This callable will be used to
|
654
|
+
apply that filter. The callable is passed the `records`, which is an `ActiveRecord::Relation`, the `value`, and an
|
655
|
+
`_options` hash. It is expected to return an `ActiveRecord::Relation`.
|
656
|
+
|
657
|
+
Note: When a filter is not supplied a `verify` callable to modify the `value` that the `apply` callable receives,
|
658
|
+
`value` defaults to an array of the string values provided to the filter parameter.
|
659
|
+
|
660
|
+
This example shows how you can implement different approaches for different filters.
|
661
|
+
|
662
|
+
```ruby
|
663
|
+
# When given the following parameter:'filter[visibility]': 'public'
|
664
|
+
|
665
|
+
filter :visibility, apply: ->(records, value, _options) {
|
666
|
+
records.where('users.publicly_visible = ?', value[0] == 'public')
|
667
|
+
}
|
668
|
+
```
|
669
|
+
|
670
|
+
If you omit the `apply` callable the filter will be applied as `records.where(filter => value)`.
|
671
|
+
|
672
|
+
Note: It is also possible to override the `self.apply_filter` method, though this approach is now deprecated:
|
673
|
+
|
674
|
+
```ruby
|
675
|
+
def self.apply_filter(records, filter, value, options)
|
676
|
+
case filter
|
677
|
+
when :last_name, :first_name, :name
|
678
|
+
if value.is_a?(Array)
|
679
|
+
value.each do |val|
|
680
|
+
records = records.where(_model_class.arel_table[filter].matches(val))
|
681
|
+
end
|
682
|
+
records
|
683
|
+
else
|
684
|
+
records.where(_model_class.arel_table[filter].matches(value))
|
685
|
+
end
|
686
|
+
else
|
687
|
+
super(records, filter, value)
|
688
|
+
end
|
689
|
+
end
|
690
|
+
```
|
691
|
+
|
692
|
+
##### Verifying Filters
|
693
|
+
|
694
|
+
Because filters typically come straight from the request, it's prudent to verify their values. To do so, provide a
|
695
|
+
callable to the `verify` option. This callable will be passed the `value` and the `context`. Verify should return the
|
696
|
+
verified value, which may be modified.
|
697
|
+
|
698
|
+
```ruby
|
699
|
+
filter :ids,
|
700
|
+
verify: ->(values, context) {
|
701
|
+
verify_keys(values, context)
|
702
|
+
values
|
703
|
+
},
|
704
|
+
apply: ->(records, value, _options) {
|
705
|
+
records.where('id IN (?)', value)
|
706
|
+
}
|
707
|
+
```
|
708
|
+
|
709
|
+
```ruby
|
710
|
+
# A more complex example, showing how to filter for any overlap between the
|
711
|
+
# value array and the possible_ids, using both verify and apply callables.
|
712
|
+
|
713
|
+
filter :possible_ids,
|
714
|
+
verify: ->(values, context) {
|
715
|
+
values.map {|value| value.to_i}
|
716
|
+
},
|
717
|
+
apply: ->(records, value, _options) {
|
718
|
+
records.where('possible_ids && ARRAY[?]', value)
|
719
|
+
}
|
720
|
+
```
|
721
|
+
|
722
|
+
##### Finders
|
723
|
+
|
724
|
+
Basic finding by filters is supported by resources. This is implemented in the `find` and `find_by_key` finder methods.
|
725
|
+
Currently this is implemented for `ActiveRecord` based resources. The finder methods rely on the `records` method to get
|
726
|
+
an `ActiveRecord::Relation` relation. It is therefore possible to override `records` to affect the three find related
|
727
|
+
methods.
|
728
|
+
|
729
|
+
###### Customizing base records for finder methods
|
730
|
+
|
731
|
+
If you need to change the base records on which `find` and `find_by_key` operate, you can override the `records` method
|
732
|
+
on the resource class.
|
733
|
+
|
734
|
+
For example to allow a user to only retrieve his own posts you can do the following:
|
735
|
+
|
736
|
+
```ruby
|
737
|
+
class PostResource < JSONAPI::Resource
|
738
|
+
attributes :title, :body
|
739
|
+
|
740
|
+
def self.records(options = {})
|
741
|
+
context = options[:context]
|
742
|
+
context[:current_user].posts
|
743
|
+
end
|
744
|
+
end
|
745
|
+
```
|
746
|
+
|
747
|
+
When you create a relationship, a method is created to fetch record(s) for that relationship, using the relation name
|
748
|
+
for the relationship.
|
749
|
+
|
750
|
+
```ruby
|
751
|
+
class PostResource < JSONAPI::Resource
|
752
|
+
has_one :author
|
753
|
+
has_many :comments
|
754
|
+
|
755
|
+
# def record_for_author
|
756
|
+
# relationship = self.class._relationship(:author)
|
757
|
+
# relation_name = relationship.relation_name(context: @context)
|
758
|
+
# records_for(relation_name)
|
759
|
+
# end
|
760
|
+
|
761
|
+
# def records_for_comments
|
762
|
+
# relationship = self.class._relationship(:comments)
|
763
|
+
# relation_name = relationship.relation_name(context: @context)
|
764
|
+
# records_for(relation_name)
|
765
|
+
# end
|
766
|
+
end
|
767
|
+
|
768
|
+
```
|
769
|
+
|
770
|
+
For example, you may want to raise an error if the user is not authorized to view the related records. See the next
|
771
|
+
section for additional details on raising errors.
|
772
|
+
|
773
|
+
```ruby
|
774
|
+
class BaseResource < JSONAPI::Resource
|
775
|
+
def records_for(relation_name)
|
776
|
+
context = options[:context]
|
777
|
+
records = _model.public_send(relation_name)
|
778
|
+
|
779
|
+
unless context[:current_user].can_view?(records)
|
780
|
+
raise NotAuthorizedError
|
781
|
+
end
|
782
|
+
|
783
|
+
records
|
784
|
+
end
|
785
|
+
end
|
786
|
+
```
|
787
|
+
|
788
|
+
###### Raising Errors
|
789
|
+
|
790
|
+
Inside the finder methods (like `records_for`) or inside of resource callbacks
|
791
|
+
(like `before_save`) you can `raise` an error to halt processing. JSONAPI::Resources
|
792
|
+
has some built in errors that will return appropriate error codes. By
|
793
|
+
default any other error that you raise will return a `500` status code
|
794
|
+
for a general internal server error.
|
795
|
+
|
796
|
+
To return useful error codes that represent application errors you
|
797
|
+
should set the `exception_class_whitelist` config variable, and then you
|
798
|
+
should use the Rails `rescue_from` macro to render a status code.
|
799
|
+
|
800
|
+
For example, this config setting allows the `NotAuthorizedError` to bubble up out of
|
801
|
+
JSONAPI::Resources and into your application.
|
802
|
+
|
803
|
+
```ruby
|
804
|
+
# config/initializer/jsonapi-resources.rb
|
805
|
+
JSONAPI.configure do |config|
|
806
|
+
config.exception_class_whitelist = [NotAuthorizedError]
|
807
|
+
end
|
808
|
+
```
|
809
|
+
|
810
|
+
Handling the error and rendering the appropriate code is now the responsibility of the
|
811
|
+
application and could be handled like this:
|
812
|
+
|
813
|
+
```ruby
|
814
|
+
class ApiController < ApplicationController
|
815
|
+
rescue_from NotAuthorizedError, with: :reject_forbidden_request
|
816
|
+
def reject_forbidden_request
|
817
|
+
render json: {error: 'Forbidden'}, :status => 403
|
818
|
+
end
|
819
|
+
end
|
820
|
+
```
|
821
|
+
|
822
|
+
|
823
|
+
###### Applying Filters
|
824
|
+
|
825
|
+
The `apply_filter` method is called to apply each filter to the `Arel` relation. You may override this method to gain
|
826
|
+
control over how the filters are applied to the `Arel` relation.
|
827
|
+
|
828
|
+
This example shows how you can implement different approaches for different filters.
|
829
|
+
|
830
|
+
```ruby
|
831
|
+
def self.apply_filter(records, filter, value, options)
|
832
|
+
case filter
|
833
|
+
when :visibility
|
834
|
+
records.where('users.publicly_visible = ?', value == :public)
|
835
|
+
when :last_name, :first_name, :name
|
836
|
+
if value.is_a?(Array)
|
837
|
+
value.each do |val|
|
838
|
+
records = records.where(_model_class.arel_table[filter].matches(val))
|
839
|
+
end
|
840
|
+
records
|
841
|
+
else
|
842
|
+
records.where(_model_class.arel_table[filter].matches(value))
|
843
|
+
end
|
844
|
+
else
|
845
|
+
super(records, filter, value)
|
846
|
+
end
|
847
|
+
end
|
848
|
+
```
|
849
|
+
|
850
|
+
|
851
|
+
###### Applying Sorting
|
852
|
+
|
853
|
+
You can override the `apply_sort` method to gain control over how the sorting is done. This may be useful in case you'd
|
854
|
+
like to base the sorting on variables in your context.
|
855
|
+
|
856
|
+
Example:
|
857
|
+
|
858
|
+
```ruby
|
859
|
+
def self.apply_sort(records, order_options, context = {})
|
860
|
+
if order_options.has?(:trending)
|
861
|
+
records = records.order_by_trending_scope
|
862
|
+
order_options - [:trending]
|
863
|
+
end
|
864
|
+
|
865
|
+
super(records, order_options, context)
|
866
|
+
end
|
867
|
+
```
|
868
|
+
|
869
|
+
|
870
|
+
###### Override finder methods
|
871
|
+
|
872
|
+
Finally if you have more complex requirements for finding you can override the `find` and `find_by_key` methods on the
|
873
|
+
resource class.
|
874
|
+
|
875
|
+
Here's an example that defers the `find` operation to a `current_user` set on the `context` option:
|
876
|
+
|
877
|
+
```ruby
|
878
|
+
class AuthorResource < JSONAPI::Resource
|
879
|
+
attribute :name
|
880
|
+
model_name 'Person'
|
881
|
+
has_many :posts
|
882
|
+
|
883
|
+
filter :name
|
884
|
+
|
885
|
+
def self.find(filters, options = {})
|
886
|
+
context = options[:context]
|
887
|
+
authors = context[:current_user].find_authors(filters)
|
888
|
+
|
889
|
+
return authors.map do |author|
|
890
|
+
self.new(author, context)
|
891
|
+
end
|
892
|
+
end
|
893
|
+
end
|
894
|
+
```
|
895
|
+
|
896
|
+
#### Pagination
|
897
|
+
|
898
|
+
Pagination is performed using a `paginator`, which is a class responsible for parsing the `page` request parameters and
|
899
|
+
applying the pagination logic to the results.
|
900
|
+
|
901
|
+
##### Paginators
|
902
|
+
|
903
|
+
`JSONAPI::Resource` supports several pagination methods by default, and allows you to implement a custom system if the
|
904
|
+
defaults do not meet your needs.
|
905
|
+
|
906
|
+
###### Paged Paginator
|
907
|
+
|
908
|
+
The `paged` `paginator` returns results based on pages of a fixed size. Valid `page` parameters are `number` and `size`.
|
909
|
+
If `number` is omitted the first page is returned. If `size` is omitted the `default_page_size` from the configuration
|
910
|
+
settings is used.
|
911
|
+
|
912
|
+
```
|
913
|
+
GET /articles?page%5Bnumber%5D=10&page%5Bsize%5D=10 HTTP/1.1
|
914
|
+
Accept: application/vnd.api+json
|
915
|
+
```
|
916
|
+
|
917
|
+
###### Offset Paginator
|
918
|
+
|
919
|
+
The `offset` `paginator` returns results based on an offset from the beginning of the resultset. Valid `page` parameters
|
920
|
+
are `offset` and `limit`. If `offset` is omitted a value of 0 will be used. If `limit` is omitted the `default_page_size`
|
921
|
+
from the configuration settings is used.
|
922
|
+
|
923
|
+
```
|
924
|
+
GET /articles?page%5Blimit%5D=10&page%5Boffset%5D=10 HTTP/1.1
|
925
|
+
Accept: application/vnd.api+json
|
926
|
+
```
|
927
|
+
|
928
|
+
###### Custom Paginators
|
929
|
+
|
930
|
+
Custom `paginators` can be used. These should derive from `Paginator`. The `apply` method takes a `relation` and
|
931
|
+
`order_options` and is expected to return a `relation`. The `initialize` method receives the parameters from the `page`
|
932
|
+
request parameters. It is up to the paginator author to parse and validate these parameters.
|
933
|
+
|
934
|
+
For example, here is a very simple single record at a time paginator:
|
935
|
+
|
936
|
+
```ruby
|
937
|
+
class SingleRecordPaginator < JSONAPI::Paginator
|
938
|
+
def initialize(params)
|
939
|
+
# param parsing and validation here
|
940
|
+
@page = params.to_i
|
941
|
+
end
|
942
|
+
|
943
|
+
def apply(relation, order_options)
|
944
|
+
relation.offset(@page).limit(1)
|
945
|
+
end
|
946
|
+
end
|
947
|
+
```
|
948
|
+
|
949
|
+
##### Paginator Configuration
|
950
|
+
|
951
|
+
The default paginator, which will be used for all resources, is set using `JSONAPI.configure`. For example, in your
|
952
|
+
`config/initializers/jsonapi_resources.rb`:
|
953
|
+
|
954
|
+
```ruby
|
955
|
+
JSONAPI.configure do |config|
|
956
|
+
# built in paginators are :none, :offset, :paged
|
957
|
+
config.default_paginator = :offset
|
958
|
+
|
959
|
+
config.default_page_size = 10
|
960
|
+
config.maximum_page_size = 20
|
961
|
+
end
|
962
|
+
```
|
963
|
+
|
964
|
+
If no `default_paginator` is configured, pagination will be disabled by default.
|
965
|
+
|
966
|
+
Paginators can also be set at the resource-level, which will override the default setting. This is done using the
|
967
|
+
`paginator` method:
|
968
|
+
|
969
|
+
```ruby
|
970
|
+
class BookResource < JSONAPI::Resource
|
971
|
+
attribute :title
|
972
|
+
attribute :isbn
|
973
|
+
|
974
|
+
paginator :offset
|
975
|
+
end
|
976
|
+
```
|
977
|
+
|
978
|
+
To disable pagination in a resource, specify `:none` for `paginator`.
|
979
|
+
|
980
|
+
#### Included relationships (side-loading resources)
|
981
|
+
|
982
|
+
JR supports [request include params](http://jsonapi.org/format/#fetching-includes) out of the box, for side loading related resources.
|
983
|
+
|
984
|
+
Here's an example from the spec:
|
985
|
+
|
986
|
+
```
|
987
|
+
GET /articles/1?include=comments HTTP/1.1
|
988
|
+
Accept: application/vnd.api+json
|
989
|
+
```
|
990
|
+
|
991
|
+
Will get you the following payload by default:
|
992
|
+
|
993
|
+
```
|
994
|
+
{
|
995
|
+
"data": {
|
996
|
+
"type": "articles",
|
997
|
+
"id": "1",
|
998
|
+
"attributes": {
|
999
|
+
"title": "JSON API paints my bikeshed!"
|
1000
|
+
},
|
1001
|
+
"links": {
|
1002
|
+
"self": "http://example.com/articles/1"
|
1003
|
+
},
|
1004
|
+
"relationships": {
|
1005
|
+
"comments": {
|
1006
|
+
"links": {
|
1007
|
+
"self": "http://example.com/articles/1/relationships/comments",
|
1008
|
+
"related": "http://example.com/articles/1/comments"
|
1009
|
+
},
|
1010
|
+
"data": [
|
1011
|
+
{ "type": "comments", "id": "5" },
|
1012
|
+
{ "type": "comments", "id": "12" }
|
1013
|
+
]
|
1014
|
+
}
|
1015
|
+
}
|
1016
|
+
},
|
1017
|
+
"included": [{
|
1018
|
+
"type": "comments",
|
1019
|
+
"id": "5",
|
1020
|
+
"attributes": {
|
1021
|
+
"body": "First!"
|
1022
|
+
},
|
1023
|
+
"links": {
|
1024
|
+
"self": "http://example.com/comments/5"
|
1025
|
+
}
|
1026
|
+
}, {
|
1027
|
+
"type": "comments",
|
1028
|
+
"id": "12",
|
1029
|
+
"attributes": {
|
1030
|
+
"body": "I like XML better"
|
1031
|
+
},
|
1032
|
+
"links": {
|
1033
|
+
"self": "http://example.com/comments/12"
|
1034
|
+
}
|
1035
|
+
}]
|
1036
|
+
}
|
1037
|
+
```
|
1038
|
+
|
1039
|
+
Note: When passing `include` and `fields` params together, relationships not included in the `fields` parameter will not be serialized. This will have the side effect of not serializing the included resources. To ensure the related resources are properly side loaded specify them in the `fields`, like `fields[posts]=comments,title&include=comments`.
|
1040
|
+
|
1041
|
+
#### Resource Meta
|
1042
|
+
|
1043
|
+
Meta information can be included for each resource using the meta method in the resource declaration. For example:
|
1044
|
+
|
1045
|
+
```ruby
|
1046
|
+
class BookResource < JSONAPI::Resource
|
1047
|
+
attribute :title
|
1048
|
+
attribute :isbn
|
1049
|
+
|
1050
|
+
def meta(options)
|
1051
|
+
{
|
1052
|
+
copyright: 'API Copyright 2015 - XYZ Corp.',
|
1053
|
+
computed_copyright: options[:serialization_options][:copyright],
|
1054
|
+
last_updated_at: _model.updated_at
|
1055
|
+
}
|
1056
|
+
end
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
```
|
1060
|
+
|
1061
|
+
The `meta` method will be called for each resource instance. Override the `meta` method on a resource class to control
|
1062
|
+
the meta information for the resource. If a non empty hash is returned from `meta` this will be serialized. The `meta`
|
1063
|
+
method is called with an `options` hash. The `options` hash will contain the following:
|
1064
|
+
|
1065
|
+
* `:serializer` -> the serializer instance
|
1066
|
+
* `:serialization_options` -> the contents of the `serialization_options` method on the controller.
|
1067
|
+
|
1068
|
+
#### Custom Links
|
1069
|
+
|
1070
|
+
Custom links can be included for each resource by overriding the `custom_links` method. If a non empty hash is returned from `custom_links`, it will be merged with the default links hash containing the resource's `self` link. The `custom_links` method is called with the same `options` hash used by for [resource meta information](#resource-meta). The `options` hash contains the following:
|
1071
|
+
|
1072
|
+
* `:serializer` -> the serializer instance
|
1073
|
+
* `:serialization_options` -> the contents of the `serialization_options` method on the controller.
|
1074
|
+
|
1075
|
+
For example:
|
1076
|
+
|
1077
|
+
```ruby
|
1078
|
+
class CityCouncilMeeting < JSONAPI::Resource
|
1079
|
+
attribute :title, :location, :approved
|
1080
|
+
|
1081
|
+
def custom_links(options)
|
1082
|
+
{ minutes: options[:serializer].link_builder.self_link(self) + "/minutes" }
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
```
|
1086
|
+
|
1087
|
+
This will create a custom link with the key `minutes`, which will be merged with the default `self` link, like so:
|
1088
|
+
|
1089
|
+
```json
|
1090
|
+
{
|
1091
|
+
"data": [
|
1092
|
+
{
|
1093
|
+
"id": "1",
|
1094
|
+
"type": "cityCouncilMeetings",
|
1095
|
+
"links": {
|
1096
|
+
"self": "http://city.gov/api/city-council-meetings/1",
|
1097
|
+
"minutes": "http://city.gov/api/city-council-meetings/1/minutes"
|
1098
|
+
},
|
1099
|
+
"attributes": {...}
|
1100
|
+
},
|
1101
|
+
//...
|
1102
|
+
]
|
1103
|
+
}
|
1104
|
+
```
|
1105
|
+
|
1106
|
+
Of course, the `custom_links` method can include logic to include links only when relevant:
|
1107
|
+
|
1108
|
+
````ruby
|
1109
|
+
class CityCouncilMeeting < JSONAPI::Resource
|
1110
|
+
attribute :title, :location, :approved
|
1111
|
+
|
1112
|
+
delegate :approved?, to: :model
|
1113
|
+
|
1114
|
+
def custom_links(options)
|
1115
|
+
extra_links = {}
|
1116
|
+
if approved?
|
1117
|
+
extra_links[:minutes] = options[:serializer].link_builder.self_link(self) + "/minutes"
|
1118
|
+
end
|
1119
|
+
extra_links
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
```
|
1123
|
+
|
1124
|
+
It's also possibly to suppress the default `self` link by returning a hash with `{self: nil}`:
|
1125
|
+
|
1126
|
+
````ruby
|
1127
|
+
class Selfless < JSONAPI::Resource
|
1128
|
+
def custom_links(options)
|
1129
|
+
{self: nil}
|
1130
|
+
end
|
1131
|
+
end
|
1132
|
+
```
|
1133
|
+
|
1134
|
+
#### Callbacks
|
1135
|
+
|
1136
|
+
`ActiveSupport::Callbacks` is used to provide callback functionality, so the behavior is very similar to what you may be
|
1137
|
+
used to from `ActiveRecord`.
|
1138
|
+
|
1139
|
+
For example, you might use a callback to perform authorization on your resource before an action.
|
1140
|
+
|
1141
|
+
```ruby
|
1142
|
+
class BaseResource < JSONAPI::Resource
|
1143
|
+
before_create :authorize_create
|
1144
|
+
|
1145
|
+
def authorize_create
|
1146
|
+
# ...
|
1147
|
+
end
|
1148
|
+
end
|
1149
|
+
```
|
1150
|
+
|
1151
|
+
The types of supported callbacks are:
|
1152
|
+
- `before`
|
1153
|
+
- `after`
|
1154
|
+
- `around`
|
1155
|
+
|
1156
|
+
##### `JSONAPI::Resource` Callbacks
|
1157
|
+
|
1158
|
+
Callbacks can be defined for the following `JSONAPI::Resource` events:
|
1159
|
+
|
1160
|
+
- `:create`
|
1161
|
+
- `:update`
|
1162
|
+
- `:remove`
|
1163
|
+
- `:save`
|
1164
|
+
- `:create_to_many_link`
|
1165
|
+
- `:replace_to_many_links`
|
1166
|
+
- `:create_to_one_link`
|
1167
|
+
- `:replace_to_one_link`
|
1168
|
+
- `:remove_to_many_link`
|
1169
|
+
- `:remove_to_one_link`
|
1170
|
+
- `:replace_fields`
|
1171
|
+
|
1172
|
+
###### Relationship Reflection
|
1173
|
+
|
1174
|
+
By default updates to relationships only invoke callbacks on the primary
|
1175
|
+
Resource. By setting the `use_relationship_reflection` [Configuration] (#configuration) option
|
1176
|
+
updates to `has_many` relationships will occur on the related resource, triggering
|
1177
|
+
callbacks on both resources.
|
1178
|
+
|
1179
|
+
##### `JSONAPI::Processor` Callbacks
|
1180
|
+
|
1181
|
+
Callbacks can also be defined for `JSONAPI::Processor` events:
|
1182
|
+
- `:operation`: Any individual operation.
|
1183
|
+
- `:find`: A `find` operation is being processed.
|
1184
|
+
- `:show`: A `show` operation is being processed.
|
1185
|
+
- `:show_relationship`: A `show_relationship` operation is being processed.
|
1186
|
+
- `:show_related_resource`: A `show_related_resource` operation is being processed.
|
1187
|
+
- `:show_related_resources`: A `show_related_resources` operation is being processed.
|
1188
|
+
- `:create_resource`: A `create_resource` operation is being processed.
|
1189
|
+
- `:remove_resource`: A `remove_resource` operation is being processed.
|
1190
|
+
- `:replace_fields`: A `replace_fields` operation is being processed.
|
1191
|
+
- `:replace_to_one_relationship`: A `replace_to_one_relationship` operation is being processed.
|
1192
|
+
- `:create_to_many_relationship`: A `create_to_many_relationship` operation is being processed.
|
1193
|
+
- `:replace_to_many_relationship`: A `replace_to_many_relationship` operation is being processed.
|
1194
|
+
- `:remove_to_many_relationship`: A `remove_to_many_relationship` operation is being processed.
|
1195
|
+
- `:remove_to_one_relationship`: A `remove_to_one_relationship` operation is being processed.
|
1196
|
+
|
1197
|
+
See [Operation Processors] (#operation-processors) for details on using OperationProcessors
|
1198
|
+
|
1199
|
+
##### `JSONAPI::OperationsProcessor` Callbacks (a removed feature)
|
1200
|
+
|
1201
|
+
Note: The `JSONAPI::OperationsProcessor` has been removed and replaced with the `JSONAPI::OperationDispatcher`
|
1202
|
+
and `Processor` classes per resource. The callbacks have been renamed and moved to the
|
1203
|
+
`Processor`s, with the exception of the `operations` callback which is now on the controller.
|
1204
|
+
|
1205
|
+
### Controllers
|
1206
|
+
|
1207
|
+
There are two ways to implement a controller for your resources. Either derive from `ResourceController` or import
|
1208
|
+
the `ActsAsResourceController` module.
|
1209
|
+
|
1210
|
+
##### ResourceController
|
1211
|
+
|
1212
|
+
`JSONAPI::Resources` provides a class, `ResourceController`, that can be used as the base class for your controllers.
|
1213
|
+
`ResourceController` supports `index`, `show`, `create`, `update`, and `destroy` methods. Just deriving your controller
|
1214
|
+
from `ResourceController` will give you a fully functional controller.
|
1215
|
+
|
1216
|
+
For example:
|
1217
|
+
|
1218
|
+
```ruby
|
1219
|
+
class PeopleController < JSONAPI::ResourceController
|
1220
|
+
|
1221
|
+
end
|
1222
|
+
```
|
1223
|
+
|
1224
|
+
Of course you are free to extend this as needed and override action handlers or other methods.
|
1225
|
+
|
1226
|
+
A jsonapi-controller generator is avaliable
|
1227
|
+
|
1228
|
+
```
|
1229
|
+
rails generate jsonapi:controller contact
|
1230
|
+
```
|
1231
|
+
|
1232
|
+
###### ResourceControllerMetal
|
1233
|
+
|
1234
|
+
`JSONAPI::Resources` also provides an alternative class to `ResourceController` called `ResourceControllerMetal`.
|
1235
|
+
In order to provide a lighter weight controller option this strips the controller down to just the classes needed
|
1236
|
+
to work with `JSONAPI::Resources`.
|
1237
|
+
|
1238
|
+
For example:
|
1239
|
+
|
1240
|
+
```ruby
|
1241
|
+
class PeopleController < JSONAPI::ResourceControllerMetal
|
1242
|
+
|
1243
|
+
end
|
1244
|
+
```
|
1245
|
+
|
1246
|
+
Note: This may not provide all of the expected controller capabilities if you are using additional gems such as DoorKeeper.
|
1247
|
+
|
1248
|
+
###### Serialization Options
|
1249
|
+
|
1250
|
+
Additional options can be passed to the serializer using the `serialization_options` method.
|
1251
|
+
|
1252
|
+
For example:
|
1253
|
+
|
1254
|
+
```ruby
|
1255
|
+
class ApplicationController < JSONAPI::ResourceController
|
1256
|
+
def serialization_options
|
1257
|
+
{copyright: 'Copyright 2015'}
|
1258
|
+
end
|
1259
|
+
end
|
1260
|
+
```
|
1261
|
+
|
1262
|
+
These `serialization_options` are passed to the `meta` method used to generate resource `meta` values.
|
1263
|
+
|
1264
|
+
##### ActsAsResourceController
|
1265
|
+
|
1266
|
+
`JSONAPI::Resources` also provides a module, `JSONAPI::ActsAsResourceController`. You can include this module to
|
1267
|
+
mix in all the features of `ResourceController` into your existing controller class.
|
1268
|
+
|
1269
|
+
For example:
|
1270
|
+
|
1271
|
+
```ruby
|
1272
|
+
class PostsController < ActionController::Base
|
1273
|
+
include JSONAPI::ActsAsResourceController
|
1274
|
+
end
|
1275
|
+
```
|
1276
|
+
|
1277
|
+
#### Namespaces
|
1278
|
+
|
1279
|
+
JSONAPI::Resources supports namespacing of controllers and resources. With namespacing you can version your API.
|
1280
|
+
|
1281
|
+
If you namespace your controller it will require a namespaced resource.
|
1282
|
+
|
1283
|
+
In the following example we have a `resource` that isn't namespaced, and one that has now been namespaced. There are
|
1284
|
+
slight differences between the two resources, as might be seen in a new version of an API:
|
1285
|
+
|
1286
|
+
```ruby
|
1287
|
+
class PostResource < JSONAPI::Resource
|
1288
|
+
attribute :title
|
1289
|
+
attribute :body
|
1290
|
+
attribute :subject
|
1291
|
+
|
1292
|
+
has_one :author, class_name: 'Person'
|
1293
|
+
has_one :section
|
1294
|
+
has_many :tags, acts_as_set: true
|
1295
|
+
has_many :comments, acts_as_set: false
|
1296
|
+
def subject
|
1297
|
+
@model.title
|
1298
|
+
end
|
1299
|
+
|
1300
|
+
filters :title, :author, :tags, :comments
|
1301
|
+
filter :id
|
1302
|
+
end
|
1303
|
+
|
1304
|
+
...
|
1305
|
+
|
1306
|
+
module Api
|
1307
|
+
module V1
|
1308
|
+
class PostResource < JSONAPI::Resource
|
1309
|
+
# V1 replaces the non-namespaced resource
|
1310
|
+
# V1 no longer supports tags and now calls author 'writer'
|
1311
|
+
attribute :title
|
1312
|
+
attribute :body
|
1313
|
+
attribute :subject
|
1314
|
+
|
1315
|
+
has_one :writer, foreign_key: 'author_id'
|
1316
|
+
has_one :section
|
1317
|
+
has_many :comments, acts_as_set: false
|
1318
|
+
|
1319
|
+
def subject
|
1320
|
+
@model.title
|
1321
|
+
end
|
1322
|
+
|
1323
|
+
filters :writer
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
class WriterResource < JSONAPI::Resource
|
1327
|
+
attributes :name, :email
|
1328
|
+
model_name 'Person'
|
1329
|
+
has_many :posts
|
1330
|
+
|
1331
|
+
filter :name
|
1332
|
+
end
|
1333
|
+
end
|
1334
|
+
end
|
1335
|
+
```
|
1336
|
+
|
1337
|
+
The following controllers are used:
|
1338
|
+
|
1339
|
+
```ruby
|
1340
|
+
class PostsController < JSONAPI::ResourceController
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
module Api
|
1344
|
+
module V1
|
1345
|
+
class PostsController < JSONAPI::ResourceController
|
1346
|
+
end
|
1347
|
+
end
|
1348
|
+
end
|
1349
|
+
```
|
1350
|
+
|
1351
|
+
You will also need to namespace your routes:
|
1352
|
+
|
1353
|
+
```ruby
|
1354
|
+
Rails.application.routes.draw do
|
1355
|
+
|
1356
|
+
jsonapi_resources :posts
|
1357
|
+
|
1358
|
+
namespace :api do
|
1359
|
+
namespace :v1 do
|
1360
|
+
jsonapi_resources :posts
|
1361
|
+
end
|
1362
|
+
end
|
1363
|
+
end
|
1364
|
+
```
|
1365
|
+
|
1366
|
+
When a namespaced `resource` is used, any related `resources` must also be in the same namespace.
|
1367
|
+
|
1368
|
+
#### Error codes
|
1369
|
+
|
1370
|
+
Error codes are provided for each error object returned, based on the error. These errors are:
|
1371
|
+
|
1372
|
+
```ruby
|
1373
|
+
module JSONAPI
|
1374
|
+
VALIDATION_ERROR = '100'
|
1375
|
+
INVALID_RESOURCE = '101'
|
1376
|
+
FILTER_NOT_ALLOWED = '102'
|
1377
|
+
INVALID_FIELD_VALUE = '103'
|
1378
|
+
INVALID_FIELD = '104'
|
1379
|
+
PARAM_NOT_ALLOWED = '105'
|
1380
|
+
PARAM_MISSING = '106'
|
1381
|
+
INVALID_FILTER_VALUE = '107'
|
1382
|
+
COUNT_MISMATCH = '108'
|
1383
|
+
KEY_ORDER_MISMATCH = '109'
|
1384
|
+
KEY_NOT_INCLUDED_IN_URL = '110'
|
1385
|
+
INVALID_INCLUDE = '112'
|
1386
|
+
RELATION_EXISTS = '113'
|
1387
|
+
INVALID_SORT_CRITERIA = '114'
|
1388
|
+
INVALID_LINKS_OBJECT = '115'
|
1389
|
+
TYPE_MISMATCH = '116'
|
1390
|
+
INVALID_PAGE_OBJECT = '117'
|
1391
|
+
INVALID_PAGE_VALUE = '118'
|
1392
|
+
INVALID_FIELD_FORMAT = '119'
|
1393
|
+
INVALID_FILTERS_SYNTAX = '120'
|
1394
|
+
SAVE_FAILED = '121'
|
1395
|
+
FORBIDDEN = '403'
|
1396
|
+
RECORD_NOT_FOUND = '404'
|
1397
|
+
NOT_ACCEPTABLE = '406'
|
1398
|
+
UNSUPPORTED_MEDIA_TYPE = '415'
|
1399
|
+
LOCKED = '423'
|
1400
|
+
end
|
1401
|
+
```
|
1402
|
+
|
1403
|
+
These codes can be customized in your app by creating an initializer to override any or all of the codes.
|
1404
|
+
|
1405
|
+
In addition textual error codes can be returned by setting the configuration option `use_text_errors = true`. For
|
1406
|
+
example:
|
1407
|
+
|
1408
|
+
```ruby
|
1409
|
+
JSONAPI.configure do |config|
|
1410
|
+
config.use_text_errors = true
|
1411
|
+
end
|
1412
|
+
```
|
1413
|
+
|
1414
|
+
|
1415
|
+
#### Handling Exceptions
|
1416
|
+
|
1417
|
+
By default, all exceptions raised downstream from a resource controller will be caught, logged, and a ```500 Internal Server Error``` will be rendered. Exceptions can be whitelisted in the config to pass through the handler and be caught manually, or you can pass a callback from a resource controller to insert logic into the rescue block without interrupting the control flow. This can be particularly useful for additional logging or monitoring without the added work of rendering responses.
|
1418
|
+
|
1419
|
+
Pass a block, refer to controller class methods, or both. Note that methods must be defined as class methods on a controller and accept one parameter, which is passed the exception object that was rescued.
|
1420
|
+
|
1421
|
+
```ruby
|
1422
|
+
class ApplicationController < JSONAPI::ResourceController
|
1423
|
+
|
1424
|
+
on_server_error :first_callback
|
1425
|
+
|
1426
|
+
#or
|
1427
|
+
|
1428
|
+
# on_server_error do |error|
|
1429
|
+
#do things
|
1430
|
+
#end
|
1431
|
+
|
1432
|
+
def self.first_callback(error)
|
1433
|
+
#env["airbrake.error_id"] = notify_airbrake(error)
|
1434
|
+
end
|
1435
|
+
end
|
1436
|
+
|
1437
|
+
```
|
1438
|
+
|
1439
|
+
#### Action Callbacks
|
1440
|
+
|
1441
|
+
##### verify_content_type_header
|
1442
|
+
|
1443
|
+
By default, when controllers extend functionalities from `jsonapi-resources`, the `ActsAsResourceController#verify_content_type_header`
|
1444
|
+
method will be triggered before `create`, `update`, `create_relationship` and `update_relationship` actions. This method is responsible
|
1445
|
+
for checking if client's request corresponds to the correct media type required by [JSON API](http://jsonapi.org/format/#content-negotiation-clients): `application/vnd.api+json`.
|
1446
|
+
|
1447
|
+
In case you need to check the media type for custom actions, just make sure to call the method in your controller's `before_action`:
|
1448
|
+
|
1449
|
+
```ruby
|
1450
|
+
class UsersController < JSONAPI::ResourceController
|
1451
|
+
before_action :verify_content_type_header, only: [:auth]
|
1452
|
+
|
1453
|
+
def auth
|
1454
|
+
# some crazy auth code goes here
|
1455
|
+
end
|
1456
|
+
end
|
1457
|
+
```
|
1458
|
+
|
1459
|
+
### Operation Processors
|
1460
|
+
|
1461
|
+
Operation Processors are called to perform the operation(s) that make up a request. The controller (through the `OperationDispatcher`), creates an `OperatorProcessor` to handle each operation. The processor is created based on the resource name, including the namespace. If a processor does not exist for a resource (namespace matters) the default operation processor is used instead. The default processor can be changed by a configuration setting.
|
1462
|
+
|
1463
|
+
Defining a custom `Processor` allows for custom callback handling of each operation type for each resource type. For example:
|
1464
|
+
|
1465
|
+
```ruby
|
1466
|
+
class Api::V4::BookProcessor < JSONAPI::Processor
|
1467
|
+
after_find do
|
1468
|
+
unless @result.is_a?(JSONAPI::ErrorsOperationResult)
|
1469
|
+
@result.meta[:total_records_found] = @result.record_count
|
1470
|
+
end
|
1471
|
+
end
|
1472
|
+
end
|
1473
|
+
```
|
1474
|
+
|
1475
|
+
This simple example uses a callback to update the result's meta property with the total count of records (a redundant
|
1476
|
+
feature only for example purposes), if there wasn't an error in the operation. It is also possible to override the
|
1477
|
+
`find` method as well if a different behavior is needed, for example:
|
1478
|
+
|
1479
|
+
```ruby
|
1480
|
+
class Api::V4::BookProcessor < JSONAPI::Processor
|
1481
|
+
def find
|
1482
|
+
filters = params[:filters]
|
1483
|
+
include_directives = params[:include_directives]
|
1484
|
+
sort_criteria = params.fetch(:sort_criteria, [])
|
1485
|
+
paginator = params[:paginator]
|
1486
|
+
|
1487
|
+
verified_filters = resource_klass.verify_filters(filters, context)
|
1488
|
+
resource_records = resource_klass.find(verified_filters,
|
1489
|
+
context: context,
|
1490
|
+
include_directives: include_directives,
|
1491
|
+
sort_criteria: sort_criteria,
|
1492
|
+
paginator: paginator)
|
1493
|
+
|
1494
|
+
page_options = {}
|
1495
|
+
# Overriding the default record count logic to always include it in the meta
|
1496
|
+
#if (JSONAPI.configuration.top_level_meta_include_record_count ||
|
1497
|
+
# (paginator && paginator.class.requires_record_count))
|
1498
|
+
page_options[:record_count] = resource_klass.find_count(verified_filters,
|
1499
|
+
context: context,
|
1500
|
+
include_directives: include_directives)
|
1501
|
+
#end
|
1502
|
+
end
|
1503
|
+
```
|
1504
|
+
|
1505
|
+
Note: The authors of this gem expect the most common uses cases to be handled using the callbacks. It is likely that the
|
1506
|
+
internal functionality of the operation processing methods will change, at least for several revisions. Effort will be
|
1507
|
+
made to call this out in release notes. You have been warned.
|
1508
|
+
|
1509
|
+
### Serializer
|
1510
|
+
|
1511
|
+
The `ResourceSerializer` can be used to serialize a resource into JSON API compliant JSON. `ResourceSerializer` must be
|
1512
|
+
initialized with the primary resource type it will be serializing. `ResourceSerializer` has a `serialize_to_hash`
|
1513
|
+
method that takes a resource instance or array of resource instances to serialize. For example:
|
1514
|
+
|
1515
|
+
```ruby
|
1516
|
+
post = Post.find(1)
|
1517
|
+
JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(PostResource.new(post, nil))
|
1518
|
+
```
|
1519
|
+
|
1520
|
+
Note: If your resource needs to access to state from a context hash, make sure to pass the context hash as the second argument of
|
1521
|
+
the resource class new method. For example:
|
1522
|
+
|
1523
|
+
```ruby
|
1524
|
+
post = Post.find(1)
|
1525
|
+
context = { current_user: current_user }
|
1526
|
+
JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(PostResource.new(post, context))
|
1527
|
+
```
|
1528
|
+
|
1529
|
+
This returns results like this:
|
1530
|
+
|
1531
|
+
```json
|
1532
|
+
{
|
1533
|
+
"data": {
|
1534
|
+
"type": "posts",
|
1535
|
+
"id": "1",
|
1536
|
+
"links": {
|
1537
|
+
"self": "http://example.com/posts/1"
|
1538
|
+
},
|
1539
|
+
"attributes": {
|
1540
|
+
"title": "New post",
|
1541
|
+
"body": "A body!!!",
|
1542
|
+
"subject": "New post"
|
1543
|
+
},
|
1544
|
+
"relationships": {
|
1545
|
+
"section": {
|
1546
|
+
"links": {
|
1547
|
+
"self": "http://example.com/posts/1/relationships/section",
|
1548
|
+
"related": "http://example.com/posts/1/section"
|
1549
|
+
},
|
1550
|
+
"data": null
|
1551
|
+
},
|
1552
|
+
"author": {
|
1553
|
+
"links": {
|
1554
|
+
"self": "http://example.com/posts/1/relationships/author",
|
1555
|
+
"related": "http://example.com/posts/1/author"
|
1556
|
+
},
|
1557
|
+
"data": {
|
1558
|
+
"type": "people",
|
1559
|
+
"id": "1"
|
1560
|
+
}
|
1561
|
+
},
|
1562
|
+
"tags": {
|
1563
|
+
"links": {
|
1564
|
+
"self": "http://example.com/posts/1/relationships/tags",
|
1565
|
+
"related": "http://example.com/posts/1/tags"
|
1566
|
+
}
|
1567
|
+
},
|
1568
|
+
"comments": {
|
1569
|
+
"links": {
|
1570
|
+
"self": "http://example.com/posts/1/relationships/comments",
|
1571
|
+
"related": "http://example.com/posts/1/comments"
|
1572
|
+
}
|
1573
|
+
}
|
1574
|
+
}
|
1575
|
+
}
|
1576
|
+
}
|
1577
|
+
```
|
1578
|
+
|
1579
|
+
#### Serializer options
|
1580
|
+
|
1581
|
+
The `ResourceSerializer` can be initialized with some optional parameters:
|
1582
|
+
|
1583
|
+
##### `include`
|
1584
|
+
|
1585
|
+
An array of resources. Nested resources can be specified with dot notation.
|
1586
|
+
|
1587
|
+
*Purpose*: determines which objects will be side loaded with the source objects in an `included` section
|
1588
|
+
|
1589
|
+
*Example*: ```include: ['comments','author','comments.tags','author.posts']```
|
1590
|
+
|
1591
|
+
##### `fields`
|
1592
|
+
|
1593
|
+
A hash of resource types and arrays of fields for each resource type.
|
1594
|
+
|
1595
|
+
*Purpose*: determines which fields are serialized for a resource type. This encompasses both attributes and
|
1596
|
+
relationship ids in the links section for a resource. Fields are global for a resource type.
|
1597
|
+
|
1598
|
+
*Example*: ```fields: { people: [:email, :comments], posts: [:title, :author], comments: [:body, :post]}```
|
1599
|
+
|
1600
|
+
```ruby
|
1601
|
+
post = Post.find(1)
|
1602
|
+
include_resources = ['comments','author','comments.tags','author.posts']
|
1603
|
+
|
1604
|
+
JSONAPI::ResourceSerializer.new(PostResource, include: include_resources,
|
1605
|
+
fields: {
|
1606
|
+
people: [:email, :comments],
|
1607
|
+
posts: [:title, :author],
|
1608
|
+
tags: [:name],
|
1609
|
+
comments: [:body, :post]
|
1610
|
+
}
|
1611
|
+
).serialize_to_hash(PostResource.new(post, nil))
|
1612
|
+
```
|
1613
|
+
|
1614
|
+
#### Formatting
|
1615
|
+
|
1616
|
+
JR by default uses some simple rules to format (and unformat) an attribute for (de-)serialization. Strings and Integers are output to JSON
|
1617
|
+
as is, and all other values have `.to_s` applied to them. This outputs something in all cases, but it is certainly not
|
1618
|
+
correct for every situation.
|
1619
|
+
|
1620
|
+
If you want to change the way an attribute is (de-)serialized you have a couple of ways. The simplest method is to create a
|
1621
|
+
getter (and setter) method on the resource which overrides the attribute and apply the (un-)formatting there. For example:
|
1622
|
+
|
1623
|
+
```ruby
|
1624
|
+
class PersonResource < JSONAPI::Resource
|
1625
|
+
attributes :name, :email, :last_login_time
|
1626
|
+
|
1627
|
+
# Setter example
|
1628
|
+
def email=(new_email)
|
1629
|
+
@model.email = new_email.downcase
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
# Getter example
|
1633
|
+
def last_login_time
|
1634
|
+
@model.last_login_time.in_time_zone(@context[:current_user].time_zone).to_s
|
1635
|
+
end
|
1636
|
+
end
|
1637
|
+
```
|
1638
|
+
|
1639
|
+
This is simple to implement for a one off situation, but not for example if you want to apply the same formatting rules
|
1640
|
+
to all DateTime fields in your system. Another issue is the attribute on the resource will always return a formatted
|
1641
|
+
response, whether you want it or not.
|
1642
|
+
|
1643
|
+
##### Value Formatters
|
1644
|
+
|
1645
|
+
To overcome the above limitations JR uses Value Formatters. Value Formatters allow you to control the way values are
|
1646
|
+
handled for an attribute. The `format` can be set per attribute as it is declared in the resource. For example:
|
1647
|
+
|
1648
|
+
```ruby
|
1649
|
+
class PersonResource < JSONAPI::Resource
|
1650
|
+
attributes :name, :email, :spoken_languages
|
1651
|
+
attribute :last_login_time, format: :date_with_utc_timezone
|
1652
|
+
|
1653
|
+
# Getter/Setter for spoken_languages ...
|
1654
|
+
end
|
1655
|
+
```
|
1656
|
+
|
1657
|
+
A Value formatter has a `format` and an `unformat` method. Here's the base ValueFormatter and DefaultValueFormatter for
|
1658
|
+
reference:
|
1659
|
+
|
1660
|
+
```ruby
|
1661
|
+
module JSONAPI
|
1662
|
+
class ValueFormatter < Formatter
|
1663
|
+
class << self
|
1664
|
+
def format(raw_value)
|
1665
|
+
super(raw_value)
|
1666
|
+
end
|
1667
|
+
|
1668
|
+
def unformat(value)
|
1669
|
+
super(value)
|
1670
|
+
end
|
1671
|
+
...
|
1672
|
+
end
|
1673
|
+
end
|
1674
|
+
end
|
1675
|
+
|
1676
|
+
class DefaultValueFormatter < JSONAPI::ValueFormatter
|
1677
|
+
class << self
|
1678
|
+
def format(raw_value)
|
1679
|
+
case raw_value
|
1680
|
+
when Date, Time, DateTime, ActiveSupport::TimeWithZone, BigDecimal
|
1681
|
+
# Use the as_json methods added to various base classes by ActiveSupport
|
1682
|
+
return raw_value.as_json
|
1683
|
+
else
|
1684
|
+
return raw_value
|
1685
|
+
end
|
1686
|
+
end
|
1687
|
+
end
|
1688
|
+
end
|
1689
|
+
```
|
1690
|
+
|
1691
|
+
You can also create your own Value Formatter. Value Formatters must be named with the `format` name followed by
|
1692
|
+
`ValueFormatter`, i.e. `DateWithUTCTimezoneValueFormatter` and derive from `JSONAPI::ValueFormatter`. It is
|
1693
|
+
recommended that you create a directory for your formatters, called `formatters`.
|
1694
|
+
|
1695
|
+
The `format` method is called by the `ResourceSerializer` as is serializing a resource. The format method takes the
|
1696
|
+
`raw_value` parameter. `raw_value` is the value as read from the model.
|
1697
|
+
|
1698
|
+
The `unformat` method is called when processing the request. Each incoming attribute (except `links`) are run through
|
1699
|
+
the `unformat` method. The `unformat` method takes a `value`, which is the value as it comes in on the
|
1700
|
+
request. This allows you process the incoming value to alter its state before it is stored in the model.
|
1701
|
+
|
1702
|
+
###### Use a Different Default Value Formatter
|
1703
|
+
|
1704
|
+
Another way to handle formatting is to set a different default value formatter. This will affect all attributes that do
|
1705
|
+
not have a `format` set. You can do this by overriding the `default_attribute_options` method for a resource (or a base
|
1706
|
+
resource for a system wide change).
|
1707
|
+
|
1708
|
+
```ruby
|
1709
|
+
def self.default_attribute_options
|
1710
|
+
{format: :my_default}
|
1711
|
+
end
|
1712
|
+
```
|
1713
|
+
|
1714
|
+
and
|
1715
|
+
|
1716
|
+
```ruby
|
1717
|
+
class MyDefaultValueFormatter < DefaultValueFormatter
|
1718
|
+
class << self
|
1719
|
+
def format(raw_value)
|
1720
|
+
case raw_value
|
1721
|
+
when DateTime
|
1722
|
+
return super(raw_value.in_time_zone('UTC'))
|
1723
|
+
else
|
1724
|
+
return super
|
1725
|
+
end
|
1726
|
+
end
|
1727
|
+
end
|
1728
|
+
end
|
1729
|
+
```
|
1730
|
+
|
1731
|
+
This way all DateTime values will be formatted to display in the UTC timezone.
|
1732
|
+
|
1733
|
+
#### Key Format
|
1734
|
+
|
1735
|
+
By default JR uses dasherized keys as per the
|
1736
|
+
[JSON API naming recommendations](http://jsonapi.org/recommendations/#naming). This can be changed by specifying a
|
1737
|
+
different key formatter.
|
1738
|
+
|
1739
|
+
For example, to use camel cased keys with an initial lowercase character (JSON's default) create an initializer and add
|
1740
|
+
the following:
|
1741
|
+
|
1742
|
+
```ruby
|
1743
|
+
JSONAPI.configure do |config|
|
1744
|
+
# built in key format options are :underscored_key, :camelized_key and :dasherized_key
|
1745
|
+
config.json_key_format = :camelized_key
|
1746
|
+
end
|
1747
|
+
```
|
1748
|
+
|
1749
|
+
This will cause the serializer to use the `CamelizedKeyFormatter`. You can also create your own `KeyFormatter`, for
|
1750
|
+
example:
|
1751
|
+
|
1752
|
+
```ruby
|
1753
|
+
class UpperCamelizedKeyFormatter < JSONAPI::KeyFormatter
|
1754
|
+
class << self
|
1755
|
+
def format(key)
|
1756
|
+
super.camelize(:upper)
|
1757
|
+
end
|
1758
|
+
end
|
1759
|
+
end
|
1760
|
+
```
|
1761
|
+
|
1762
|
+
You would specify this in `JSONAPI.configure` as `:upper_camelized`.
|
1763
|
+
|
1764
|
+
### Routing
|
1765
|
+
|
1766
|
+
JR has a couple of helper methods available to assist you with setting up routes.
|
1767
|
+
|
1768
|
+
##### `jsonapi_resources`
|
1769
|
+
|
1770
|
+
Like `resources` in `ActionDispatch`, `jsonapi_resources` provides resourceful routes mapping between HTTP verbs and URLs
|
1771
|
+
and controller actions. This will also setup mappings for relationship URLs for a resource's relationships. For example:
|
1772
|
+
|
1773
|
+
```ruby
|
1774
|
+
Rails.application.routes.draw do
|
1775
|
+
jsonapi_resources :contacts
|
1776
|
+
jsonapi_resources :phone_numbers
|
1777
|
+
end
|
1778
|
+
```
|
1779
|
+
|
1780
|
+
gives the following routes
|
1781
|
+
|
1782
|
+
```
|
1783
|
+
Prefix Verb URI Pattern Controller#Action
|
1784
|
+
contact_relationships_phone_numbers GET /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#show_relationship {:relationship=>"phone_numbers"}
|
1785
|
+
POST /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#create_relationship {:relationship=>"phone_numbers"}
|
1786
|
+
DELETE /contacts/:contact_id/relationships/phone-numbers/:keys(.:format) contacts#destroy_relationship {:relationship=>"phone_numbers"}
|
1787
|
+
contact_phone_numbers GET /contacts/:contact_id/phone-numbers(.:format) phone_numbers#get_related_resources {:relationship=>"phone_numbers", :source=>"contacts"}
|
1788
|
+
contacts GET /contacts(.:format) contacts#index
|
1789
|
+
POST /contacts(.:format) contacts#create
|
1790
|
+
contact GET /contacts/:id(.:format) contacts#show
|
1791
|
+
PATCH /contacts/:id(.:format) contacts#update
|
1792
|
+
PUT /contacts/:id(.:format) contacts#update
|
1793
|
+
DELETE /contacts/:id(.:format) contacts#destroy
|
1794
|
+
phone_number_relationships_contact GET /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#show_relationship {:relationship=>"contact"}
|
1795
|
+
PUT|PATCH /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#update_relationship {:relationship=>"contact"}
|
1796
|
+
DELETE /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#destroy_relationship {:relationship=>"contact"}
|
1797
|
+
phone_number_contact GET /phone-numbers/:phone_number_id/contact(.:format) contacts#get_related_resource {:relationship=>"contact", :source=>"phone_numbers"}
|
1798
|
+
phone_numbers GET /phone-numbers(.:format) phone_numbers#index
|
1799
|
+
POST /phone-numbers(.:format) phone_numbers#create
|
1800
|
+
phone_number GET /phone-numbers/:id(.:format) phone_numbers#show
|
1801
|
+
PATCH /phone-numbers/:id(.:format) phone_numbers#update
|
1802
|
+
PUT /phone-numbers/:id(.:format) phone_numbers#update
|
1803
|
+
DELETE /phone-numbers/:id(.:format) phone_numbers#destroy
|
1804
|
+
```
|
1805
|
+
|
1806
|
+
##### `jsonapi_resource`
|
1807
|
+
|
1808
|
+
Like `jsonapi_resources`, but for resources you lookup without an id.
|
1809
|
+
|
1810
|
+
#### Nested Routes
|
1811
|
+
|
1812
|
+
By default nested routes are created for getting related resources and manipulating relationships. You can control the
|
1813
|
+
nested routes by passing a block into `jsonapi_resources` or `jsonapi_resource`. An empty block will not create
|
1814
|
+
any nested routes. For example:
|
1815
|
+
|
1816
|
+
```ruby
|
1817
|
+
Rails.application.routes.draw do
|
1818
|
+
jsonapi_resources :contacts do
|
1819
|
+
end
|
1820
|
+
end
|
1821
|
+
```
|
1822
|
+
|
1823
|
+
gives routes that are only related to the primary resource, and none for its relationships:
|
1824
|
+
|
1825
|
+
```
|
1826
|
+
Prefix Verb URI Pattern Controller#Action
|
1827
|
+
contacts GET /contacts(.:format) contacts#index
|
1828
|
+
POST /contacts(.:format) contacts#create
|
1829
|
+
contact GET /contacts/:id(.:format) contacts#show
|
1830
|
+
PATCH /contacts/:id(.:format) contacts#update
|
1831
|
+
PUT /contacts/:id(.:format) contacts#update
|
1832
|
+
DELETE /contacts/:id(.:format) contacts#destroy
|
1833
|
+
```
|
1834
|
+
|
1835
|
+
To manually add in the nested routes you can use the `jsonapi_links`, `jsonapi_related_resources` and
|
1836
|
+
`jsonapi_related_resource` inside the block. Or, you can add the default set of nested routes using the
|
1837
|
+
`jsonapi_relationships` method. For example:
|
1838
|
+
|
1839
|
+
```ruby
|
1840
|
+
Rails.application.routes.draw do
|
1841
|
+
jsonapi_resources :contacts do
|
1842
|
+
jsonapi_relationships
|
1843
|
+
end
|
1844
|
+
end
|
1845
|
+
```
|
1846
|
+
|
1847
|
+
###### `jsonapi_links`
|
1848
|
+
|
1849
|
+
You can add relationship routes in with `jsonapi_links`, for example:
|
1850
|
+
|
1851
|
+
```ruby
|
1852
|
+
Rails.application.routes.draw do
|
1853
|
+
jsonapi_resources :contacts do
|
1854
|
+
jsonapi_links :phone_numbers
|
1855
|
+
end
|
1856
|
+
end
|
1857
|
+
```
|
1858
|
+
|
1859
|
+
Gives the following routes:
|
1860
|
+
|
1861
|
+
```
|
1862
|
+
contact_relationships_phone_numbers GET /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#show_relationship {:relationship=>"phone_numbers"}
|
1863
|
+
POST /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#create_relationship {:relationship=>"phone_numbers"}
|
1864
|
+
DELETE /contacts/:contact_id/relationships/phone-numbers/:keys(.:format) contacts#destroy_relationship {:relationship=>"phone_numbers"}
|
1865
|
+
contacts GET /contacts(.:format) contacts#index
|
1866
|
+
POST /contacts(.:format) contacts#create
|
1867
|
+
contact GET /contacts/:id(.:format) contacts#show
|
1868
|
+
PATCH /contacts/:id(.:format) contacts#update
|
1869
|
+
PUT /contacts/:id(.:format) contacts#update
|
1870
|
+
DELETE /contacts/:id(.:format) contacts#destroy
|
1871
|
+
|
1872
|
+
```
|
1873
|
+
|
1874
|
+
The new routes allow you to show, create and destroy the relationships between resources.
|
1875
|
+
|
1876
|
+
###### `jsonapi_related_resources`
|
1877
|
+
|
1878
|
+
Creates a nested route to GET the related has_many resources. For example:
|
1879
|
+
|
1880
|
+
```ruby
|
1881
|
+
Rails.application.routes.draw do
|
1882
|
+
jsonapi_resources :contacts do
|
1883
|
+
jsonapi_related_resources :phone_numbers
|
1884
|
+
end
|
1885
|
+
end
|
1886
|
+
|
1887
|
+
```
|
1888
|
+
|
1889
|
+
gives the following routes:
|
1890
|
+
|
1891
|
+
```
|
1892
|
+
Prefix Verb URI Pattern Controller#Action
|
1893
|
+
contact_phone_numbers GET /contacts/:contact_id/phone-numbers(.:format) phone_numbers#get_related_resources {:relationship=>"phone_numbers", :source=>"contacts"}
|
1894
|
+
contacts GET /contacts(.:format) contacts#index
|
1895
|
+
POST /contacts(.:format) contacts#create
|
1896
|
+
contact GET /contacts/:id(.:format) contacts#show
|
1897
|
+
PATCH /contacts/:id(.:format) contacts#update
|
1898
|
+
PUT /contacts/:id(.:format) contacts#update
|
1899
|
+
DELETE /contacts/:id(.:format) contacts#destroy
|
1900
|
+
|
1901
|
+
```
|
1902
|
+
|
1903
|
+
A single additional route was created to allow you GET the phone numbers through the contact.
|
1904
|
+
|
1905
|
+
###### `jsonapi_related_resource`
|
1906
|
+
|
1907
|
+
Like `jsonapi_related_resources`, but for has_one related resources.
|
1908
|
+
|
1909
|
+
```ruby
|
1910
|
+
Rails.application.routes.draw do
|
1911
|
+
jsonapi_resources :phone_numbers do
|
1912
|
+
jsonapi_related_resource :contact
|
1913
|
+
end
|
1914
|
+
end
|
1915
|
+
```
|
1916
|
+
|
1917
|
+
gives the following routes:
|
1918
|
+
|
1919
|
+
```
|
1920
|
+
Prefix Verb URI Pattern Controller#Action
|
1921
|
+
phone_number_contact GET /phone-numbers/:phone_number_id/contact(.:format) contacts#get_related_resource {:relationship=>"contact", :source=>"phone_numbers"}
|
1922
|
+
phone_numbers GET /phone-numbers(.:format) phone_numbers#index
|
1923
|
+
POST /phone-numbers(.:format) phone_numbers#create
|
1924
|
+
phone_number GET /phone-numbers/:id(.:format) phone_numbers#show
|
1925
|
+
PATCH /phone-numbers/:id(.:format) phone_numbers#update
|
1926
|
+
PUT /phone-numbers/:id(.:format) phone_numbers#update
|
1927
|
+
DELETE /phone-numbers/:id(.:format) phone_numbers#destroy
|
1928
|
+
|
1929
|
+
```
|
1930
|
+
|
1931
|
+
### Authorization
|
1932
|
+
|
1933
|
+
Currently `json-api-resources` doesn't come with built-in primitives for authorization. However multiple users of the framework have come up with different approaches, check out:
|
1934
|
+
|
1935
|
+
- [jsonapi-authorization](https://github.com/venuu/jsonapi-authorization)
|
1936
|
+
- [pundit-resources](https://github.com/togglepro/pundit-resources)
|
1937
|
+
|
1938
|
+
Refer to the comments/discussion [here](https://github.com/cerebris/jsonapi-resources/issues/16#issuecomment-222438975) for the differences between approaches
|
1939
|
+
|
1940
|
+
### Resource Caching
|
1941
|
+
|
1942
|
+
To improve the response time of GET requests, JR can cache the generated JSON fragments for
|
1943
|
+
Resources which are suitable. First, set `config.resource_cache` to an ActiveSupport cache store:
|
1944
|
+
|
1945
|
+
```ruby
|
1946
|
+
JSONAPI.configure do |config|
|
1947
|
+
config.resource_cache = Rails.cache
|
1948
|
+
end
|
1949
|
+
```
|
1950
|
+
|
1951
|
+
Then, on each Resource you want to cache, call the `caching` method:
|
1952
|
+
|
1953
|
+
```ruby
|
1954
|
+
class PostResource < JSONAPI::Resource
|
1955
|
+
caching
|
1956
|
+
end
|
1957
|
+
```
|
1958
|
+
|
1959
|
+
See the caveats section below for situations where you might not want to enable caching on particular
|
1960
|
+
Resources.
|
1961
|
+
|
1962
|
+
The Resource model must also have a field that is updated whenever any of the model's data changes.
|
1963
|
+
The default Rails timestamps handle this pretty well, and the default cache key field is `updated_at` for this reason.
|
1964
|
+
You can use an alternate field (which you are then responsible for updating) by calling the `cache_field` method:
|
1965
|
+
|
1966
|
+
```ruby
|
1967
|
+
class PostResource < JSONAPI::Resource
|
1968
|
+
caching
|
1969
|
+
cache_field :change_counter
|
1970
|
+
|
1971
|
+
before_save do
|
1972
|
+
if self.change_counter.nil?
|
1973
|
+
self.change_counter = 1
|
1974
|
+
elsif self.changed?
|
1975
|
+
self.change_counter += 1
|
1976
|
+
end
|
1977
|
+
end
|
1978
|
+
|
1979
|
+
after_touch do
|
1980
|
+
update_attribute(:change_counter, self.change_counter + 1)
|
1981
|
+
end
|
1982
|
+
end
|
1983
|
+
```
|
1984
|
+
|
1985
|
+
If context affects the content of the serialized result, you must define a class method `attribute_caching_context` on that Resource, which should return a different value for contexts that produce different results. In particular, if the `meta` or `fetchable_fields` methods, or any method providing the actual content of an attribute, changes depending on context, then you must provide `attribute_caching_context`. The actual value it
|
1986
|
+
returns isn't important, what matters is that the value must be different if any relevant part of the context is different.
|
1987
|
+
|
1988
|
+
```ruby
|
1989
|
+
class PostResource < JSONAPI::Resource
|
1990
|
+
caching
|
1991
|
+
|
1992
|
+
attributes :title, :body, :secret_field
|
1993
|
+
|
1994
|
+
def fetchable_fields
|
1995
|
+
return super if context.user.superuser?
|
1996
|
+
return super - [:secret_field]
|
1997
|
+
end
|
1998
|
+
|
1999
|
+
def meta
|
2000
|
+
if context.user.can_see_creation_dates?
|
2001
|
+
return { created: _model.created_at }
|
2002
|
+
else
|
2003
|
+
return {}
|
2004
|
+
end
|
2005
|
+
end
|
2006
|
+
|
2007
|
+
def self.attribute_caching_context(context)
|
2008
|
+
return {
|
2009
|
+
admin: context.user.superuser?,
|
2010
|
+
creation_date_viewer: context.user.can_see_creation_dates?
|
2011
|
+
}
|
2012
|
+
end
|
2013
|
+
end
|
2014
|
+
```
|
2015
|
+
|
2016
|
+
#### Caching Caveats
|
2017
|
+
|
2018
|
+
* Models for cached Resources must update a cache key field whenever their data changes. However, if you bypass Rails and e.g. alter the database row directly without changing the `updated_at` field, the cached entry for that resource will be inaccurate. Also, `updated_at` provides a narrow race condition window; if a resource is updated twice in the same second, it's possible that only the first update will be cached. If you're concerned about this, you will need to find a way to make sure your models' cache fields change on every update, e.g. by using a unique random value or a monotonic clock.
|
2019
|
+
* If an attribute's value is affected by related resources, e.g. the `spoken_languages` example above, then changes to the related resource must also touch the cache field on the resource that uses it. The `belongs_to` relation in ActiveRecord provides a `:touch` option for this purpose.
|
2020
|
+
* JR does not actively clean the cache, so you must use an ActiveSupport cache that automatically expires old entries, or you will leak resources. The MemoryCache built in to Rails does this by default, but other caches will have to be configured with an `:expires_in` option and/or a cache-specific clearing mechanism.
|
2021
|
+
* Similarly, if you make a substantial code change that affects a lot of serialized representations (i.e. changing the way an attribute is shown), you'll have to clear out all relevant cache entries yourself. The simplest way to do this is to run `JSONAPI.configuration.resource_cache.clear` from the console. You do not have to do this after merely adding or removing attributes; only changes that affect the actual content of attributes require manual cache clearing.
|
2022
|
+
* If resource caching is enabled at all, then custom relationship methods on any resource might not always be used, even resources that are not cached. For example, if you manually define a `comments` method or `records_for_comments` method on a Resource that `has_many :comments`, you cannot expect it to be used when caching is enabled, even if you never call `caching` on that particular Resource. Instead, you should use relationship name lambdas.
|
2023
|
+
* The above also applies to custom `find` or `find_by_key` methods. Instead, if you are using resource caching anywhere in your app, try overriding the `find_records` method to return an appropriate `ActiveRecord::Relation`.
|
2024
|
+
* Caching relies on ActiveRecord features; you cannot enable caching on resources based on non-AR models, e.g. PORO objects or singleton resources.
|
2025
|
+
* If you write a custom `ResourceSerializer` which takes new options, then you must define `config_description` to include those options if they might impact the serialized value:
|
2026
|
+
|
2027
|
+
```ruby
|
2028
|
+
class MySerializer < JSONAPI::ResourceSerializer
|
2029
|
+
def initialize(primary_resource_klass, options = {})
|
2030
|
+
@my_special_option = options.delete(:my_special_option)
|
2031
|
+
super
|
2032
|
+
end
|
2033
|
+
|
2034
|
+
def config_description(resource_klass)
|
2035
|
+
super.merge({my_special_option: @my_special_option})
|
2036
|
+
end
|
2037
|
+
end
|
2038
|
+
```
|
2039
|
+
|
2040
|
+
## Configuration
|
2041
|
+
|
2042
|
+
JR has a few configuration options. Some have already been mentioned above. To set configuration options create an
|
2043
|
+
initializer and add the options you wish to set. All options have defaults, so you only need to set the options that
|
2044
|
+
are different. The default options are shown below.
|
2045
|
+
|
2046
|
+
If using custom classes (such as a CustomPaginator), be sure to require them at the top of the initializer before usage.
|
2047
|
+
|
2048
|
+
```ruby
|
2049
|
+
JSONAPI.configure do |config|
|
2050
|
+
#:underscored_key, :camelized_key, :dasherized_key, or custom
|
2051
|
+
config.json_key_format = :dasherized_key
|
2052
|
+
|
2053
|
+
#:underscored_route, :camelized_route, :dasherized_route, or custom
|
2054
|
+
config.route_format = :dasherized_route
|
2055
|
+
|
2056
|
+
# Default Processor, used if a resource specific one is not defined.
|
2057
|
+
# Must be a class
|
2058
|
+
config.default_processor_klass = JSONAPI::Processor
|
2059
|
+
|
2060
|
+
#:integer, :uuid, :string, or custom (provide a proc)
|
2061
|
+
config.resource_key_type = :integer
|
2062
|
+
|
2063
|
+
# optional request features
|
2064
|
+
config.allow_include = true
|
2065
|
+
config.allow_sort = true
|
2066
|
+
config.allow_filter = true
|
2067
|
+
|
2068
|
+
# How to handle unsupported attributes and relationships which are provided in the request
|
2069
|
+
# true => raises an error
|
2070
|
+
# false => allows the request to continue. A warning is included in the response meta data indicating
|
2071
|
+
# the fields which were ignored. This is useful for client libraries which send extra parameters.
|
2072
|
+
config.raise_if_parameters_not_allowed = true
|
2073
|
+
|
2074
|
+
# :none, :offset, :paged, or a custom paginator name
|
2075
|
+
config.default_paginator = :none
|
2076
|
+
|
2077
|
+
# Output pagination links at top level
|
2078
|
+
config.top_level_links_include_pagination = true
|
2079
|
+
|
2080
|
+
config.default_page_size = 10
|
2081
|
+
config.maximum_page_size = 20
|
2082
|
+
|
2083
|
+
# Output the record count in top level meta data for find operations
|
2084
|
+
config.top_level_meta_include_record_count = false
|
2085
|
+
config.top_level_meta_record_count_key = :record_count
|
2086
|
+
|
2087
|
+
# For :paged paginators, the following are also available
|
2088
|
+
config.top_level_meta_include_page_count = false
|
2089
|
+
config.top_level_meta_page_count_key = :page_count
|
2090
|
+
|
2091
|
+
config.use_text_errors = false
|
2092
|
+
|
2093
|
+
# List of classes that should not be rescued by the operations processor.
|
2094
|
+
# For example, if you use Pundit for authorization, you might
|
2095
|
+
# raise a Pundit::NotAuthorizedError at some point during operations
|
2096
|
+
# processing. If you want to use Rails' `rescue_from` macro to
|
2097
|
+
# catch this error and render a 403 status code, you should add
|
2098
|
+
# the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.
|
2099
|
+
# Subclasses of the whitelisted classes will also be whitelisted.
|
2100
|
+
config.exception_class_whitelist = []
|
2101
|
+
|
2102
|
+
# If enabled, will override configuration option `exception_class_whitelist`
|
2103
|
+
# and whitelist all exceptions.
|
2104
|
+
config.whitelist_all_exceptions = false
|
2105
|
+
|
2106
|
+
# Resource Linkage
|
2107
|
+
# Controls the serialization of resource linkage for non compound documents
|
2108
|
+
# NOTE: always_include_to_many_linkage_data is not currently implemented
|
2109
|
+
config.always_include_to_one_linkage_data = false
|
2110
|
+
|
2111
|
+
# Relationship reflection invokes the related resource when updates
|
2112
|
+
# are made to a has_many relationship. By default relationship_reflection
|
2113
|
+
# is turned off because it imposes a small performance penalty.
|
2114
|
+
config.use_relationship_reflection = false
|
2115
|
+
|
2116
|
+
# Allows transactions for creating and updating records
|
2117
|
+
# Set this to false if your backend does not support transactions (e.g. Mongodb)
|
2118
|
+
config.allow_transactions = true
|
2119
|
+
|
2120
|
+
# Formatter Caching
|
2121
|
+
# Set to false to disable caching of string operations on keys and links.
|
2122
|
+
# Note that unlike the resource cache, formatter caching is always done
|
2123
|
+
# internally in-memory and per-thread; no ActiveSupport::Cache is used.
|
2124
|
+
config.cache_formatters = true
|
2125
|
+
|
2126
|
+
# Resource cache
|
2127
|
+
# An ActiveSupport::Cache::Store or similar, used by Resources with caching enabled.
|
2128
|
+
# Set to `nil` (the default) to disable caching, or to `Rails.cache` to use the
|
2129
|
+
# Rails cache store.
|
2130
|
+
config.resource_cache = nil
|
2131
|
+
|
2132
|
+
# Default resource cache field
|
2133
|
+
# On Resources with caching enabled, this field will be used to check for out-of-date
|
2134
|
+
# cache entries, unless overridden on a specific Resource. Defaults to "updated_at".
|
2135
|
+
config.default_resource_cache_field = :updated_at
|
2136
|
+
|
2137
|
+
# Resource cache digest function
|
2138
|
+
# Provide a callable that returns a unique value for string inputs with
|
2139
|
+
# low chance of collision. The default is SHA256 base64.
|
2140
|
+
config.resource_cache_digest_function = Digest::SHA2.new.method(:base64digest)
|
2141
|
+
|
2142
|
+
# Resource cache usage reporting
|
2143
|
+
# Optionally provide a callable which JSONAPI will call with information about cache
|
2144
|
+
# performance. Should accept three arguments: resource name, hits count, misses count.
|
2145
|
+
config.resource_cache_usage_report_function = nil
|
2146
|
+
end
|
2147
|
+
```
|
42
2148
|
|
43
2149
|
## Contributing
|
44
2150
|
|
@@ -48,6 +2154,16 @@ Or install it yourself as:
|
|
48
2154
|
4. Push to the branch (`git push origin my-new-feature`)
|
49
2155
|
5. Create a new Pull Request
|
50
2156
|
|
2157
|
+
### Running Tests
|
2158
|
+
|
2159
|
+
To run the tests for this project:
|
2160
|
+
|
2161
|
+
- `rake test` or `bundle exec rake test`
|
2162
|
+
|
2163
|
+
To run a single test:
|
2164
|
+
|
2165
|
+
- `bundle exec ruby -I test test/controllers/controller_test.rb -n test_type_formatting`
|
2166
|
+
|
51
2167
|
## License
|
52
2168
|
|
53
2169
|
Copyright 2014-2016 Cerebris Corporation. MIT License (see LICENSE for details).
|