jsonapi-resources 0.0.14 → 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/Gemfile +1 -3
- data/README.md +118 -23
- data/lib/jsonapi/formatter.rb +10 -1
- data/lib/jsonapi/request.rb +6 -6
- data/lib/jsonapi/resource.rb +8 -4
- data/lib/jsonapi/resource_controller.rb +1 -1
- data/lib/jsonapi/resource_for.rb +1 -1
- data/lib/jsonapi/resource_serializer.rb +22 -5
- data/lib/jsonapi/resources/version.rb +1 -1
- data/test/controllers/controller_test.rb +95 -43
- data/test/fixtures/active_record.rb +49 -5
- data/test/unit/serializer/serializer_test.rb +129 -110
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 526bbf206d79bc7971772b890f0734ab2af6b426
|
4
|
+
data.tar.gz: 015940fbcc6439e1616bbfc538c63e213d5544da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9470953f1434a9ae0fff2de51f730cc0357369f7240b07a0edc7d948440b843e52e20f78e19dcf7e28f3b18a7ba8fd31a5136f53d9919603d47d8b1fe174201
|
7
|
+
data.tar.gz: 23b05b382b869d38a26937b9c9c2c8d75811543e456e6c37963d498b9f06d9e644ab5ca8318afcee4d6b27c84e379453037cef50dc28027cebb60a182a811212
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# JSONAPI::Resources [![Build Status](https://secure.travis-ci.org/cerebris/jsonapi-resources.png?branch=master)](http://travis-ci.org/cerebris/jsonapi-resources)
|
2
2
|
|
3
|
-
JSONAPI::Resources
|
3
|
+
`JSONAPI::Resources`, or "JR", provides a framework for developing a server that complies with the [JSON API](http://jsonapi.org/) specification.
|
4
4
|
|
5
5
|
Like JSON API itself, JR's design is focused on the resources served by an API. JR needs little more than a definition of your resources, including their attributes and relationships, to make your server compliant with JSON API.
|
6
6
|
|
@@ -63,7 +63,7 @@ end
|
|
63
63
|
|
64
64
|
This resource has 5 attributes: `:id`, `:name_first`, `:name_last`, `:email`, `:twitter`. By default these attributes must exist on the model that is handled by the resource.
|
65
65
|
|
66
|
-
A resource object wraps a Ruby object, usually an ActiveModel record, which is available as the `@model` variable. This allows a resource's methods to access the underlying model.
|
66
|
+
A resource object wraps a Ruby object, usually an `ActiveModel` record, which is available as the `@model` variable. This allows a resource's methods to access the underlying model.
|
67
67
|
|
68
68
|
For example, a computed attribute for `full_name` could be defined as such:
|
69
69
|
|
@@ -102,7 +102,7 @@ class AuthorResource < JSONAPI::Resource
|
|
102
102
|
end
|
103
103
|
```
|
104
104
|
|
105
|
-
Context flows through from the controller and can be used to control the attributes based on the current user (or other value)
|
105
|
+
Context flows through from the controller and can be used to control the attributes based on the current user (or other value).
|
106
106
|
|
107
107
|
##### Creatable and Updateable Attributes
|
108
108
|
|
@@ -152,9 +152,9 @@ end
|
|
152
152
|
|
153
153
|
##### Attribute Formatting
|
154
154
|
|
155
|
-
Attributes can have a Format
|
155
|
+
Attributes can have a `Format`. By default all attributes use the default formatter. If an attribute has the `format` option set the system will attempt to find a formatter based on this name. In the following example the `last_login_time` will be returned formatted to a certain time zone:
|
156
156
|
|
157
|
-
```
|
157
|
+
```ruby
|
158
158
|
class PersonResource < JSONAPI::Resource
|
159
159
|
attributes :id, :name, :email
|
160
160
|
attribute :last_login_time, format: :date_with_timezone
|
@@ -235,7 +235,7 @@ Examples:
|
|
235
235
|
class ExpenseEntryResource < JSONAPI::Resource
|
236
236
|
attributes :id, :cost, :transaction_date
|
237
237
|
|
238
|
-
has_one :currency, class_name: 'Currency',
|
238
|
+
has_one :currency, class_name: 'Currency', foreign_key: 'currency_code'
|
239
239
|
has_one :employee
|
240
240
|
end
|
241
241
|
```
|
@@ -260,7 +260,7 @@ end
|
|
260
260
|
|
261
261
|
##### Finders
|
262
262
|
|
263
|
-
Basic finding by filters is supported by resources. This is implemented in the `find`, `find_by_key` and `find_by_keys` finder methods. Currently this is implemented for ActiveRecord based resources. The finder methods rely on the `records` method to get an Arel relation. It is therefore possible to override `records` to affect the three find related methods.
|
263
|
+
Basic finding by filters is supported by resources. This is implemented in the `find`, `find_by_key` and `find_by_keys` finder methods. Currently this is implemented for `ActiveRecord` based resources. The finder methods rely on the `records` method to get an `Arel` relation. It is therefore possible to override `records` to affect the three find related methods.
|
264
264
|
|
265
265
|
###### Customizing base records for finder methods
|
266
266
|
|
@@ -281,9 +281,9 @@ end
|
|
281
281
|
|
282
282
|
###### Applying Filters
|
283
283
|
|
284
|
-
The `apply_filter` method is called to apply each filter to the Arel relation. You may override this method to gain control over how the filters are applied to the Arel relation.
|
284
|
+
The `apply_filter` method is called to apply each filter to the `Arel` relation. You may override this method to gain control over how the filters are applied to the `Arel` relation.
|
285
285
|
|
286
|
-
|
286
|
+
This example shows how you can implement different approaches for different filters.
|
287
287
|
|
288
288
|
```ruby
|
289
289
|
def apply_filter(records, filter, value)
|
@@ -332,7 +332,7 @@ end
|
|
332
332
|
|
333
333
|
### Controllers
|
334
334
|
|
335
|
-
JSONAPI::Resources provides a class, `ResourceController`, that can be used as the base class for your controllers. `ResourceController` supports `index`, `show`, `create`, `update`, and `destroy` methods. Just deriving your controller from `ResourceController` will give you a fully functional controller.
|
335
|
+
`JSONAPI::Resources` provides a class, `ResourceController`, that can be used as the base class for your controllers. `ResourceController` supports `index`, `show`, `create`, `update`, and `destroy` methods. Just deriving your controller from `ResourceController` will give you a fully functional controller.
|
336
336
|
|
337
337
|
For example:
|
338
338
|
|
@@ -362,6 +362,98 @@ class PeopleController < ApplicationController
|
|
362
362
|
end
|
363
363
|
```
|
364
364
|
|
365
|
+
#### Namespaces
|
366
|
+
|
367
|
+
JSONAPI::Resources supports namespacing of controllers and resources. With namespacing you can version your API.
|
368
|
+
|
369
|
+
If you namespace your controller it will require a namespaced resource.
|
370
|
+
|
371
|
+
In the following example we have a `resource` that isn't namespaced, and one the has now been namespaced. There are slight differences between the two resources, as might be seen in a new version of an API:
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
class PostResource < JSONAPI::Resource
|
375
|
+
attribute :id
|
376
|
+
attribute :title
|
377
|
+
attribute :body
|
378
|
+
attribute :subject
|
379
|
+
|
380
|
+
has_one :author, class_name: 'Person'
|
381
|
+
has_one :section
|
382
|
+
has_many :tags, acts_as_set: true
|
383
|
+
has_many :comments, acts_as_set: false
|
384
|
+
def subject
|
385
|
+
@model.title
|
386
|
+
end
|
387
|
+
|
388
|
+
filters :title, :author, :tags, :comments
|
389
|
+
filter :id
|
390
|
+
end
|
391
|
+
|
392
|
+
...
|
393
|
+
|
394
|
+
module Api
|
395
|
+
module V1
|
396
|
+
class PostResource < JSONAPI::Resource
|
397
|
+
# V1 replaces the non-namespaced resource
|
398
|
+
# V1 no longer supports tags and now calls author 'writer'
|
399
|
+
attribute :id
|
400
|
+
attribute :title
|
401
|
+
attribute :body
|
402
|
+
attribute :subject
|
403
|
+
|
404
|
+
has_one :writer, foreign_key: 'author_id'
|
405
|
+
has_one :section
|
406
|
+
has_many :comments, acts_as_set: false
|
407
|
+
|
408
|
+
def subject
|
409
|
+
@model.title
|
410
|
+
end
|
411
|
+
|
412
|
+
filters :writer
|
413
|
+
end
|
414
|
+
|
415
|
+
class WriterResource < JSONAPI::Resource
|
416
|
+
attributes :id, :name, :email
|
417
|
+
model_name 'Person'
|
418
|
+
has_many :posts
|
419
|
+
|
420
|
+
filter :name
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
```
|
425
|
+
|
426
|
+
The following controllers are used:
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
class PostsController < JSONAPI::ResourceController
|
430
|
+
end
|
431
|
+
|
432
|
+
module Api
|
433
|
+
module V1
|
434
|
+
class PostsController < JSONAPI::ResourceController
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
```
|
439
|
+
|
440
|
+
You will also need to namespace your routes:
|
441
|
+
|
442
|
+
```ruby
|
443
|
+
Rails.application.routes.draw do
|
444
|
+
|
445
|
+
jsonapi_resources :posts
|
446
|
+
|
447
|
+
namespace :api do
|
448
|
+
namespace :v1 do
|
449
|
+
jsonapi_resources :posts
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
```
|
454
|
+
|
455
|
+
When a namespaced `resource` is used, any related `resources` must also be in the same namespace.
|
456
|
+
|
365
457
|
#### Error codes
|
366
458
|
|
367
459
|
Error codes are provided for each error object returned, based on the error. These errors are:
|
@@ -420,7 +512,7 @@ This returns results like this:
|
|
420
512
|
}
|
421
513
|
```
|
422
514
|
|
423
|
-
####
|
515
|
+
#### serialize_to_hash method options
|
424
516
|
|
425
517
|
The `serialize_to_hash` method also takes some optional parameters:
|
426
518
|
|
@@ -442,13 +534,16 @@ A hash of resource types and arrays of fields for each resource type.
|
|
442
534
|
|
443
535
|
```ruby
|
444
536
|
post = Post.find(1)
|
445
|
-
JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
537
|
+
JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(
|
538
|
+
PostResource.new(post),
|
539
|
+
include: ['comments','author','comments.tags','author.posts'],
|
540
|
+
fields: {
|
541
|
+
people: [:id, :email, :comments],
|
542
|
+
posts: [:id, :title, :author],
|
543
|
+
tags: [:name],
|
544
|
+
comments: [:id, :body, :post]
|
545
|
+
}
|
546
|
+
)
|
452
547
|
```
|
453
548
|
|
454
549
|
##### `context`
|
@@ -461,7 +556,7 @@ JR has a couple of helper methods available to assist you with setting up routes
|
|
461
556
|
|
462
557
|
##### `jsonapi_resources`
|
463
558
|
|
464
|
-
Like `resources` in ActionDispatch
|
559
|
+
Like `resources` in `ActionDispatch`, `jsonapi_resources` provides resourceful routes mapping between HTTP verbs and URLs and controller actions. This will also setup mappings for relationship URLs for a resource's associations. For example:
|
465
560
|
|
466
561
|
```ruby
|
467
562
|
require 'jsonapi/routing_ext'
|
@@ -586,13 +681,13 @@ end
|
|
586
681
|
|
587
682
|
You can also create your own Value Formatter. Value Formatters must be named with the `format` name followed by `ValueFormatter`, i.e. `DateWithTimezoneValueFormatter` and derive from `JSONAPI::ValueFormatter`. It is recommended that you create a directory for your formatters, called `formatters`.
|
588
683
|
|
589
|
-
The `format` method is called by the ResourceSerializer as is serializing a resource. The format method takes the `raw_value`, and `context` parameters. `raw_value` is the value as read from the model, and `context` is the context of the current user/request. From this you can base the formatted version of the attribute current context.
|
684
|
+
The `format` method is called by the `ResourceSerializer` as is serializing a resource. The format method takes the `raw_value`, and `context` parameters. `raw_value` is the value as read from the model, and `context` is the context of the current user/request. From this you can base the formatted version of the attribute current context.
|
590
685
|
|
591
686
|
The `unformat` method is called when processing the request. Each incoming attribute (except `links`) are run through the `unformat` method. The `unformat` method takes the `value`, and `context` parameters. `value` is the value as it comes in on the request, and `context` is the context of the current user/request. This allows you process the incoming value to alter its state before it is stored in the model. By default no processing is applied.
|
592
687
|
|
593
688
|
###### Use a Different Default Value Formatter
|
594
689
|
|
595
|
-
Another way to handle formatting is to set a different default value formatter. This will affect all attributes that do
|
690
|
+
Another way to handle formatting is to set a different default value formatter. This will affect all attributes that do not have a `format` set. You can do this by overriding the `default_attribute_options` method for a resource (or a base resource for a system wide change).
|
596
691
|
|
597
692
|
```ruby
|
598
693
|
def default_attribute_options
|
@@ -623,7 +718,7 @@ This way all DateTime values will be formatted to display in the specified timez
|
|
623
718
|
|
624
719
|
#### Key Format
|
625
720
|
|
626
|
-
JSONAPI is agnostic on the format of the keys used in the responses. By default JR uses underscored keys which match the attribute names used by
|
721
|
+
JSONAPI is agnostic on the format of the keys used in the responses. By default JR uses underscored keys which match the attribute names used by Rails models. This can be changed by specifying a different key formatter.
|
627
722
|
|
628
723
|
For example to use camel cased keys with an initial lowercase character (JSON's default) create an initializer and add the following:
|
629
724
|
|
@@ -634,7 +729,7 @@ JSONAPI.configure do |config|
|
|
634
729
|
end
|
635
730
|
```
|
636
731
|
|
637
|
-
This will cause the serializer to use the CamelizedKeyFormatter
|
732
|
+
This will cause the serializer to use the `CamelizedKeyFormatter`. Besides `UnderscoredKeyFormatter` and `CamelizedKeyFormatter` JR defines the `DasherizedKeyFormatter`. You can also create your own `KeyFormatter`, for example:
|
638
733
|
|
639
734
|
```ruby
|
640
735
|
class UpperCamelizedKeyFormatter < JSONAPI::KeyFormatter
|
data/lib/jsonapi/formatter.rb
CHANGED
@@ -102,6 +102,15 @@ class DefaultValueFormatter < JSONAPI::ValueFormatter
|
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
+
class IdValueFormatter < JSONAPI::ValueFormatter
|
106
|
+
class << self
|
107
|
+
def format(raw_value, context)
|
108
|
+
return if raw_value.nil?
|
109
|
+
raw_value.to_s
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
105
114
|
class UnderscoredRouteFormatter < JSONAPI::RouteFormatter
|
106
115
|
end
|
107
116
|
|
@@ -127,4 +136,4 @@ class DasherizedRouteFormatter < JSONAPI::RouteFormatter
|
|
127
136
|
formatted_route.to_s.underscore.to_sym
|
128
137
|
end
|
129
138
|
end
|
130
|
-
end
|
139
|
+
end
|
data/lib/jsonapi/request.rb
CHANGED
@@ -20,7 +20,7 @@ module JSONAPI
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def setup(params)
|
23
|
-
@resource_klass ||= self.class.resource_for(params[:controller]
|
23
|
+
@resource_klass ||= self.class.resource_for(params[:controller]) if params[:controller]
|
24
24
|
|
25
25
|
unless params.nil?
|
26
26
|
case params[:action]
|
@@ -77,7 +77,7 @@ module JSONAPI
|
|
77
77
|
underscored_type = unformat_key(type)
|
78
78
|
fields[type] = []
|
79
79
|
begin
|
80
|
-
type_resource = self.class.resource_for(underscored_type)
|
80
|
+
type_resource = self.class.resource_for(@resource_klass.module_path + underscored_type.to_s)
|
81
81
|
rescue NameError
|
82
82
|
@errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
|
83
83
|
end
|
@@ -109,7 +109,7 @@ module JSONAPI
|
|
109
109
|
association = resource_klass._association(association_name)
|
110
110
|
if association
|
111
111
|
unless include_parts.last.empty?
|
112
|
-
check_include(Resource.resource_for(association.class_name), include_parts.last.partition('.'))
|
112
|
+
check_include(Resource.resource_for(@resource_klass.module_path + association.class_name.to_s), include_parts.last.partition('.'))
|
113
113
|
end
|
114
114
|
else
|
115
115
|
@errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
|
@@ -219,13 +219,13 @@ module JSONAPI
|
|
219
219
|
association = @resource_klass._association(param)
|
220
220
|
|
221
221
|
if association.is_a?(JSONAPI::Association::HasOne)
|
222
|
-
checked_has_one_associations[param] = @resource_klass.resource_for(association.type).verify_key(value, @context)
|
222
|
+
checked_has_one_associations[param] = @resource_klass.resource_for(@resource_klass.module_path + association.type.to_s).verify_key(value, @context)
|
223
223
|
elsif association.is_a?(JSONAPI::Association::HasMany)
|
224
224
|
keys = []
|
225
225
|
if value.is_a?(Array)
|
226
|
-
keys = @resource_klass.resource_for(association.type).verify_keys(value, @context)
|
226
|
+
keys = @resource_klass.resource_for(@resource_klass.module_path + association.type.to_s).verify_keys(value, @context)
|
227
227
|
else
|
228
|
-
keys.push(@resource_klass.resource_for(association.type).verify_key(value, @context))
|
228
|
+
keys.push(@resource_klass.resource_for(@resource_klass.module_path + association.type.to_s).verify_key(value, @context))
|
229
229
|
end
|
230
230
|
checked_has_many_associations[param] = keys
|
231
231
|
else
|
data/lib/jsonapi/resource.rb
CHANGED
@@ -398,6 +398,10 @@ module JSONAPI
|
|
398
398
|
_allowed_filters.include?(filter)
|
399
399
|
end
|
400
400
|
|
401
|
+
def module_path
|
402
|
+
@module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
|
403
|
+
end
|
404
|
+
|
401
405
|
private
|
402
406
|
|
403
407
|
def check_reserved_resource_name(type, name)
|
@@ -441,8 +445,8 @@ module JSONAPI
|
|
441
445
|
|
442
446
|
if @_associations[attr].is_a?(JSONAPI::Association::HasOne)
|
443
447
|
define_method attr do
|
444
|
-
type_name = self.class._associations[attr].type
|
445
|
-
resource_class = self.class.resource_for(type_name)
|
448
|
+
type_name = self.class._associations[attr].type.to_s
|
449
|
+
resource_class = self.class.resource_for(self.class.module_path + type_name)
|
446
450
|
if resource_class
|
447
451
|
associated_model = @model.send attr
|
448
452
|
return associated_model ? resource_class.new(associated_model, @context) : nil
|
@@ -450,8 +454,8 @@ module JSONAPI
|
|
450
454
|
end unless method_defined?(attr)
|
451
455
|
elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
|
452
456
|
define_method attr do
|
453
|
-
type_name = self.class._associations[attr].type
|
454
|
-
resource_class = self.class.resource_for(type_name)
|
457
|
+
type_name = self.class._associations[attr].type.to_s
|
458
|
+
resource_class = self.class.resource_for(self.class.module_path + type_name)
|
455
459
|
resources = []
|
456
460
|
if resource_class
|
457
461
|
associated_models = @model.send attr
|
@@ -104,7 +104,7 @@ module JSONAPI
|
|
104
104
|
# :nocov:
|
105
105
|
|
106
106
|
def resource_klass_name
|
107
|
-
@resource_klass_name ||= "#{self.class.name.
|
107
|
+
@resource_klass_name ||= "#{self.class.name.sub(/Controller$/, '').singularize}Resource"
|
108
108
|
end
|
109
109
|
|
110
110
|
def setup_request
|
data/lib/jsonapi/resource_for.rb
CHANGED
@@ -9,7 +9,7 @@ module JSONAPI
|
|
9
9
|
if RUBY_VERSION >= '2.0'
|
10
10
|
def resource_for(type)
|
11
11
|
resource_name = JSONAPI::Resource._resource_name_from_type(type)
|
12
|
-
Object.const_get
|
12
|
+
Object.const_get(resource_name, false) if resource_name
|
13
13
|
rescue NameError
|
14
14
|
raise NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
|
15
15
|
end
|
@@ -127,9 +127,15 @@ module JSONAPI
|
|
127
127
|
end
|
128
128
|
|
129
129
|
fields.each_with_object({}) do |name, hash|
|
130
|
-
|
131
|
-
|
132
|
-
|
130
|
+
format = source.class._attribute_options(name)[:format]
|
131
|
+
if format == :default && name == :id
|
132
|
+
format = 'id'
|
133
|
+
end
|
134
|
+
hash[format_key(name)] = format_value(
|
135
|
+
source.send(name),
|
136
|
+
format,
|
137
|
+
source
|
138
|
+
)
|
133
139
|
end
|
134
140
|
end
|
135
141
|
|
@@ -148,10 +154,9 @@ module JSONAPI
|
|
148
154
|
included_associations = source.fetchable_fields & associations.keys
|
149
155
|
associations.each_with_object({}) do |(name, association), hash|
|
150
156
|
if included_associations.include? name
|
151
|
-
foreign_key = association.foreign_key
|
152
157
|
|
153
158
|
if field_set.include?(name)
|
154
|
-
hash[format_key(name)] = source
|
159
|
+
hash[format_key(name)] = foreign_key_value(source, association)
|
155
160
|
end
|
156
161
|
|
157
162
|
ia = requested_associations.is_a?(Hash) ? requested_associations[name] : nil
|
@@ -198,6 +203,18 @@ module JSONAPI
|
|
198
203
|
return @linked_objects.key?(type) && @linked_objects[type].key?(id)
|
199
204
|
end
|
200
205
|
|
206
|
+
# Extracts the foreign key value for an association.
|
207
|
+
def foreign_key_value(source, association)
|
208
|
+
foreign_key = association.foreign_key
|
209
|
+
value = source.send(foreign_key)
|
210
|
+
|
211
|
+
if association.is_a?(JSONAPI::Association::HasMany)
|
212
|
+
value.map { |value| IdValueFormatter.format(value, {}) }
|
213
|
+
elsif association.is_a?(JSONAPI::Association::HasOne)
|
214
|
+
IdValueFormatter.format(value, {})
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
201
218
|
# Sets that an object should be included in the primary document of the response.
|
202
219
|
def set_primary(type, id)
|
203
220
|
type = format_key(type)
|