jsonapi-resources 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +10 -9
- data/README.md +156 -14
- data/lib/generators/jsonapi/resource_generator.rb +1 -1
- data/lib/generators/jsonapi/templates/jsonapi_resource.rb +1 -1
- data/lib/jsonapi/configuration.rb +8 -0
- data/lib/jsonapi/error.rb +2 -1
- data/lib/jsonapi/exceptions.rb +1 -1
- data/lib/jsonapi/operation.rb +1 -1
- data/lib/jsonapi/relationship.rb +1 -1
- data/lib/jsonapi/request.rb +59 -47
- data/lib/jsonapi/resource.rb +47 -5
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/routing_ext.rb +4 -0
- data/test/fixtures/active_record.rb +30 -14
- data/test/fixtures/customers.yml +4 -0
- data/test/fixtures/line_items.yml +7 -1
- data/test/fixtures/purchase_orders.yml +6 -0
- data/test/integration/requests/request_test.rb +84 -0
- data/test/integration/routes/routes_test.rb +11 -0
- data/test/lib/generators/jsonapi/resource_generator_test.rb +5 -0
- data/test/test_helper.rb +22 -0
- data/test/unit/resource/resource_test.rb +80 -0
- 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: 61e3e8cacf54569eb7d57771d08cdd4861a84a22
|
4
|
+
data.tar.gz: 25ccc4dc6996b991af26ebc7598129615ee14b4c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20e94f28d19e31142b2f80c562356a87d17f6d49df68918b4ff9be66a4528b1f734b86a0f98b25fafa566d4c2e3565d08f9cca6ec34878c32b8e40d3e580f029
|
7
|
+
data.tar.gz: 52bcc55a0d1200315129584fb978c75cbcd67bdcebb546cb8a62d137cc4d21f95cf9b1b938303a2dbbe603bb785f265d4f3d2b0b7d6c25746a717de40d5629fe
|
data/Gemfile
CHANGED
@@ -11,12 +11,13 @@ platforms :jruby do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
version = ENV['RAILS_VERSION'] || 'default'
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
gem 'rails',
|
14
|
+
|
15
|
+
case version
|
16
|
+
when 'master'
|
17
|
+
gem 'rails', { git: 'https://github.com/rails/rails.git' }
|
18
|
+
gem 'arel', { git: 'https://github.com/rails/arel.git' }
|
19
|
+
when 'default'
|
20
|
+
gem 'rails', '>= 4.2'
|
21
|
+
else
|
22
|
+
gem 'rails', "~> #{version}"
|
23
|
+
end
|
data/README.md
CHANGED
@@ -9,6 +9,30 @@ of your resources, including their attributes and relationships, to make your se
|
|
9
9
|
JR is designed to work with Rails 4.0+, and provides custom routes, controllers, and serializers. JR's resources may be
|
10
10
|
backed by ActiveRecord models or by custom objects.
|
11
11
|
|
12
|
+
## Table of Contents
|
13
|
+
|
14
|
+
* [Demo App] (#demo-app)
|
15
|
+
* [Client Libraries] (#client-libraries)
|
16
|
+
* [Installation] (#installation)
|
17
|
+
* [Usage] (#usage)
|
18
|
+
* [Resources] (#resources)
|
19
|
+
* [JSONAPI::Resource] (#jsonapiresource)
|
20
|
+
* [Attributes] (#attributes)
|
21
|
+
* [Primary Key] (#primary-key)
|
22
|
+
* [Model Name] (#model-name)
|
23
|
+
* [Relationships] (#relationships)
|
24
|
+
* [Filters] (#filters)
|
25
|
+
* [Pagination] (#pagination)
|
26
|
+
* [Included relationships (side-loading resources)] (#included-relationships-side-loading-resources)
|
27
|
+
* [Callbacks] (#callbacks)
|
28
|
+
* [Controllers] (#controllers)
|
29
|
+
* [Namespaces] (#namespaces)
|
30
|
+
* [Error Codes] (#error-codes)
|
31
|
+
* [Serializer] (#serializer)
|
32
|
+
* [Configuration] (#configuration)
|
33
|
+
* [Contributing] (#contributing)
|
34
|
+
* [License] (#license)
|
35
|
+
|
12
36
|
## Demo App
|
13
37
|
|
14
38
|
We have a simple demo app, called [Peeps](https://github.com/cerebris/peeps), available to show how JR is used.
|
@@ -56,12 +80,12 @@ end
|
|
56
80
|
|
57
81
|
A jsonapi-resource generator is avaliable
|
58
82
|
```
|
59
|
-
rails generate jsonapi:resource contact
|
83
|
+
rails generate jsonapi:resource contact
|
60
84
|
```
|
61
85
|
|
62
86
|
##### Abstract Resources
|
63
87
|
|
64
|
-
Resources that are not backed by a model (purely used as base classes for other resources) should be declared as
|
88
|
+
Resources that are not backed by a model (purely used as base classes for other resources) should be declared as
|
65
89
|
abstract.
|
66
90
|
|
67
91
|
Because abstract resources do not expect to be backed by a model, they won't attempt to discover the model class
|
@@ -70,7 +94,7 @@ or any of its relationships.
|
|
70
94
|
```ruby
|
71
95
|
class BaseResource < JSONAPI::Resource
|
72
96
|
abstract
|
73
|
-
|
97
|
+
|
74
98
|
has_one :creator
|
75
99
|
end
|
76
100
|
|
@@ -207,19 +231,36 @@ If the underlying model does not use `id` as the primary key _and_ does not supp
|
|
207
231
|
must use the `primary_key` method to tell the resource which field on the model to use as the primary key. **Note:**
|
208
232
|
this _must_ be the actual primary key of the model.
|
209
233
|
|
210
|
-
By default only integer values are allowed for primary key. To change this behavior you can
|
211
|
-
|
234
|
+
By default only integer values are allowed for primary key. To change this behavior you can set the `resource_key_type`
|
235
|
+
configuration option:
|
212
236
|
|
213
237
|
```ruby
|
214
|
-
|
215
|
-
|
216
|
-
|
238
|
+
JSONAPI.configure do |config|
|
239
|
+
# Allowed values are :integer(default), :uuid, :string, or a proc
|
240
|
+
config.resource_key_type = :uuid
|
241
|
+
end
|
242
|
+
```
|
217
243
|
|
218
|
-
|
244
|
+
##### Override key type on a resource
|
219
245
|
|
220
|
-
|
221
|
-
|
222
|
-
|
246
|
+
You can override the default resource key type on a per-resource basis by calling `key_type` in the resource class,
|
247
|
+
with the same allowed values as the `resource_key_type` configuration option.
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
class ContactResource < JSONAPI::Resource
|
251
|
+
attribute :id
|
252
|
+
attributes :name_first, :name_last, :email, :twitter
|
253
|
+
key_type :uuid
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
##### Custom resource key validators
|
258
|
+
|
259
|
+
If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that accepts key and context arguments in `config/initializers/jsonapi_resources.rb`:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
JSONAPI.configure do |config|
|
263
|
+
config.resource_key_type = -> (key, context) { key && String(key) }
|
223
264
|
end
|
224
265
|
```
|
225
266
|
|
@@ -427,7 +468,8 @@ end
|
|
427
468
|
|
428
469
|
```
|
429
470
|
|
430
|
-
For example, you may want raise an error if the user is not authorized to view the related records.
|
471
|
+
For example, you may want raise an error if the user is not authorized to view the related records. See the next
|
472
|
+
section for additional details on raising errors.
|
431
473
|
|
432
474
|
```ruby
|
433
475
|
class BaseResource < JSONAPI::Resource
|
@@ -444,6 +486,42 @@ class BaseResource < JSONAPI::Resource
|
|
444
486
|
end
|
445
487
|
```
|
446
488
|
|
489
|
+
|
490
|
+
###### Raising Errors
|
491
|
+
|
492
|
+
Inside the finder methods (like `records_for`) or inside of resource callbacks
|
493
|
+
(like `before_save`) you can `raise` an error to halt processing. JSONAPI::Resources
|
494
|
+
has some built in errors that will return appropriate error codes. By
|
495
|
+
default any other error that you raise will return a `500` status code
|
496
|
+
for a general internal server error.
|
497
|
+
|
498
|
+
To return useful error codes that represent application errors you
|
499
|
+
should set the `exception_class_whitelist` config varible, and then you
|
500
|
+
should use the Rails `rescue_from` macro to render a status code.
|
501
|
+
|
502
|
+
For example, this config setting allows the `NotAuthorizedError` to bubble up out of
|
503
|
+
JSONAPI::Resources and into your application.
|
504
|
+
|
505
|
+
```ruby
|
506
|
+
# config/initializer/jsonapi-resources.rb
|
507
|
+
JSONAPI.configure do |config|
|
508
|
+
config.exception_class_whitelist = [NotAuthorizedError]
|
509
|
+
end
|
510
|
+
```
|
511
|
+
|
512
|
+
Handling the error and rendering the appropriate code is now the resonsiblity of the
|
513
|
+
application and could be handled like this:
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
class ApiController < ApplicationController
|
517
|
+
rescue_from NotAuthorizedError, with: :reject_forbidden_request
|
518
|
+
def reject_forbidden_request
|
519
|
+
render json: {error: 'Forbidden'}, :status => 403
|
520
|
+
end
|
521
|
+
end
|
522
|
+
```
|
523
|
+
|
524
|
+
|
447
525
|
###### Applying Filters
|
448
526
|
|
449
527
|
The `apply_filter` method is called to apply each filter to the `Arel` relation. You may override this method to gain
|
@@ -571,6 +649,67 @@ end
|
|
571
649
|
|
572
650
|
To disable pagination in a resource, specify `:none` for `paginator`.
|
573
651
|
|
652
|
+
#### Included relationships (side-loading resources)
|
653
|
+
|
654
|
+
JR supports [request include params](http://jsonapi.org/format/#fetching-includes) out of the box, for side loading related resources.
|
655
|
+
|
656
|
+
Here's an example from the spec:
|
657
|
+
|
658
|
+
```
|
659
|
+
GET /articles/1?include=comments HTTP/1.1
|
660
|
+
Accept: application/vnd.api+json
|
661
|
+
```
|
662
|
+
|
663
|
+
Will get you the following payload by default:
|
664
|
+
|
665
|
+
```
|
666
|
+
{
|
667
|
+
"data": {
|
668
|
+
"type": "articles",
|
669
|
+
"id": "1",
|
670
|
+
"attributes": {
|
671
|
+
"title": "JSON API paints my bikeshed!"
|
672
|
+
},
|
673
|
+
"links": {
|
674
|
+
"self": "http://example.com/articles/1"
|
675
|
+
},
|
676
|
+
"relationships": {
|
677
|
+
"comments": {
|
678
|
+
"links": {
|
679
|
+
"self": "http://example.com/articles/1/relationships/comments",
|
680
|
+
"related": "http://example.com/articles/1/comments"
|
681
|
+
},
|
682
|
+
"data": [
|
683
|
+
{ "type": "comments", "id": "5" },
|
684
|
+
{ "type": "comments", "id": "12" }
|
685
|
+
]
|
686
|
+
}
|
687
|
+
}
|
688
|
+
},
|
689
|
+
"included": [{
|
690
|
+
"type": "comments",
|
691
|
+
"id": "5",
|
692
|
+
"attributes": {
|
693
|
+
"body": "First!"
|
694
|
+
},
|
695
|
+
"links": {
|
696
|
+
"self": "http://example.com/comments/5"
|
697
|
+
}
|
698
|
+
}, {
|
699
|
+
"type": "comments",
|
700
|
+
"id": "12",
|
701
|
+
"attributes": {
|
702
|
+
"body": "I like XML better"
|
703
|
+
},
|
704
|
+
"links": {
|
705
|
+
"self": "http://example.com/comments/12"
|
706
|
+
}
|
707
|
+
}]
|
708
|
+
}
|
709
|
+
```
|
710
|
+
|
711
|
+
You can also pass an `include` option to [Serializer#serialize_to_hash](#include) if you want to define this inline.
|
712
|
+
|
574
713
|
#### Callbacks
|
575
714
|
|
576
715
|
`ActiveSupport::Callbacks` is used to provide callback functionality, so the behavior is very similar to what you may be
|
@@ -845,7 +984,7 @@ example:
|
|
845
984
|
|
846
985
|
```ruby
|
847
986
|
JSONAPI.configure do |config|
|
848
|
-
config.use_text_errors =
|
987
|
+
config.use_text_errors = true
|
849
988
|
end
|
850
989
|
```
|
851
990
|
|
@@ -1277,6 +1416,9 @@ JSONAPI.configure do |config|
|
|
1277
1416
|
#:basic, :active_record, or custom
|
1278
1417
|
config.operations_processor = :active_record
|
1279
1418
|
|
1419
|
+
#:integer, :uuid, :string, or custom (provide a proc)
|
1420
|
+
config.resource_key_type = :integer
|
1421
|
+
|
1280
1422
|
# optional request features
|
1281
1423
|
config.allow_include = true
|
1282
1424
|
config.allow_sort = true
|
@@ -5,6 +5,7 @@ require 'jsonapi/active_record_operations_processor'
|
|
5
5
|
module JSONAPI
|
6
6
|
class Configuration
|
7
7
|
attr_reader :json_key_format,
|
8
|
+
:resource_key_type,
|
8
9
|
:key_formatter,
|
9
10
|
:route_format,
|
10
11
|
:route_formatter,
|
@@ -34,6 +35,9 @@ module JSONAPI
|
|
34
35
|
#:basic, :active_record, or custom
|
35
36
|
self.operations_processor = :active_record
|
36
37
|
|
38
|
+
#:integer, :uuid, :string, or custom (provide a proc)
|
39
|
+
self.resource_key_type = :integer
|
40
|
+
|
37
41
|
# optional request features
|
38
42
|
self.allow_include = true
|
39
43
|
self.allow_sort = true
|
@@ -77,6 +81,10 @@ module JSONAPI
|
|
77
81
|
@key_formatter = JSONAPI::Formatter.formatter_for(format)
|
78
82
|
end
|
79
83
|
|
84
|
+
def resource_key_type=(key_type)
|
85
|
+
@resource_key_type = key_type
|
86
|
+
end
|
87
|
+
|
80
88
|
def route_format=(format)
|
81
89
|
@route_format = format
|
82
90
|
@route_formatter = JSONAPI::Formatter.formatter_for(format)
|
data/lib/jsonapi/error.rb
CHANGED
data/lib/jsonapi/exceptions.rb
CHANGED
data/lib/jsonapi/operation.rb
CHANGED
@@ -158,7 +158,7 @@ module JSONAPI
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def records
|
161
|
-
related_resource_records = source_resource.public_send(@relationship_type)
|
161
|
+
related_resource_records = source_resource.public_send("records_for_" + @relationship_type)
|
162
162
|
@resource_klass.filter_records(@filters, @options, related_resource_records)
|
163
163
|
end
|
164
164
|
|
data/lib/jsonapi/relationship.rb
CHANGED
data/lib/jsonapi/request.rb
CHANGED
@@ -406,55 +406,12 @@ module JSONAPI
|
|
406
406
|
value.each do |link_key, link_value|
|
407
407
|
param = unformat_key(link_key)
|
408
408
|
relationship = @resource_klass._relationship(param)
|
409
|
-
if relationship.is_a?(JSONAPI::Relationship::ToOne)
|
410
|
-
if link_value.nil?
|
411
|
-
linkage = nil
|
412
|
-
else
|
413
|
-
linkage = link_value[:data]
|
414
|
-
end
|
415
|
-
|
416
|
-
links_object = parse_to_one_links_object(linkage)
|
417
|
-
if !relationship.polymorphic? && links_object[:type] && (links_object[:type].to_s != relationship.type.to_s)
|
418
|
-
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
|
419
|
-
end
|
420
409
|
|
421
|
-
|
422
|
-
|
423
|
-
relationship_id = relationship_resource.verify_key(links_object[:id], @context)
|
424
|
-
if relationship.polymorphic?
|
425
|
-
checked_to_one_relationships[param] = { id: relationship_id, type: unformat_key(links_object[:type].to_s) }
|
426
|
-
else
|
427
|
-
checked_to_one_relationships[param] = relationship_id
|
428
|
-
end
|
429
|
-
else
|
430
|
-
checked_to_one_relationships[param] = nil
|
431
|
-
end
|
410
|
+
if relationship.is_a?(JSONAPI::Relationship::ToOne)
|
411
|
+
checked_to_one_relationships[param] = parse_to_one_relationship(link_value, relationship)
|
432
412
|
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
|
433
|
-
|
434
|
-
|
435
|
-
elsif link_value.is_a?(Hash)
|
436
|
-
linkage = link_value[:data]
|
437
|
-
else
|
438
|
-
fail JSONAPI::Exceptions::InvalidLinksObject.new
|
439
|
-
end
|
440
|
-
|
441
|
-
links_object = parse_to_many_links_object(linkage)
|
442
|
-
|
443
|
-
# Since we do not yet support polymorphic relationships we will raise an error if the type does not match the
|
444
|
-
# relationship's type.
|
445
|
-
# ToDo: Support Polymorphic relationships
|
446
|
-
|
447
|
-
if links_object.length == 0
|
448
|
-
checked_to_many_relationships[param] = []
|
449
|
-
else
|
450
|
-
if links_object.length > 1 || !links_object.has_key?(unformat_key(relationship.type).to_s)
|
451
|
-
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
|
452
|
-
end
|
453
|
-
|
454
|
-
links_object.each_pair do |type, keys|
|
455
|
-
relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(type).to_s)
|
456
|
-
checked_to_many_relationships[param] = relationship_resource.verify_keys(keys, @context)
|
457
|
-
end
|
413
|
+
parse_to_many_relationship(link_value, relationship) do |result_val|
|
414
|
+
checked_to_many_relationships[param] = result_val
|
458
415
|
end
|
459
416
|
end
|
460
417
|
end
|
@@ -475,6 +432,61 @@ module JSONAPI
|
|
475
432
|
}.deep_transform_keys { |key| unformat_key(key) }
|
476
433
|
end
|
477
434
|
|
435
|
+
def parse_to_one_relationship(link_value, relationship)
|
436
|
+
if link_value.nil?
|
437
|
+
linkage = nil
|
438
|
+
else
|
439
|
+
linkage = link_value[:data]
|
440
|
+
end
|
441
|
+
|
442
|
+
links_object = parse_to_one_links_object(linkage)
|
443
|
+
if !relationship.polymorphic? && links_object[:type] && (links_object[:type].to_s != relationship.type.to_s)
|
444
|
+
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
|
445
|
+
end
|
446
|
+
|
447
|
+
unless links_object[:id].nil?
|
448
|
+
resource = self.resource_klass || Resource
|
449
|
+
relationship_resource = resource.resource_for(@resource_klass.module_path + unformat_key(links_object[:type]).to_s)
|
450
|
+
relationship_id = relationship_resource.verify_key(links_object[:id], @context)
|
451
|
+
if relationship.polymorphic?
|
452
|
+
{ id: relationship_id, type: unformat_key(links_object[:type].to_s) }
|
453
|
+
else
|
454
|
+
relationship_id
|
455
|
+
end
|
456
|
+
else
|
457
|
+
nil
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def parse_to_many_relationship(link_value, relationship, &add_result)
|
462
|
+
if link_value.is_a?(Array) && link_value.length == 0
|
463
|
+
linkage = []
|
464
|
+
elsif link_value.is_a?(Hash)
|
465
|
+
linkage = link_value[:data]
|
466
|
+
else
|
467
|
+
fail JSONAPI::Exceptions::InvalidLinksObject.new
|
468
|
+
end
|
469
|
+
|
470
|
+
links_object = parse_to_many_links_object(linkage)
|
471
|
+
|
472
|
+
# Since we do not yet support polymorphic to_many relationships we will raise an error if the type does not match the
|
473
|
+
# relationship's type.
|
474
|
+
# ToDo: Support Polymorphic relationships
|
475
|
+
|
476
|
+
if links_object.length == 0
|
477
|
+
add_result.call([])
|
478
|
+
else
|
479
|
+
if links_object.length > 1 || !links_object.has_key?(unformat_key(relationship.type).to_s)
|
480
|
+
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
|
481
|
+
end
|
482
|
+
|
483
|
+
links_object.each_pair do |type, keys|
|
484
|
+
relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(type).to_s)
|
485
|
+
add_result.call relationship_resource.verify_keys(keys, @context)
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
478
490
|
def unformat_value(attribute, value)
|
479
491
|
value_formatter = JSONAPI::ValueFormatter.value_formatter_for(@resource_klass._attribute_options(attribute)[:format])
|
480
492
|
value_formatter.unformat(value)
|
data/lib/jsonapi/resource.rb
CHANGED
@@ -165,10 +165,11 @@ module JSONAPI
|
|
165
165
|
relationship_key_values.each do |relationship_key_value|
|
166
166
|
related_resource = relationship.resource_klass.find_by_key(relationship_key_value, context: @context)
|
167
167
|
|
168
|
+
relation_name = relationship.relation_name(context: @context)
|
168
169
|
# TODO: Add option to skip relations that already exist instead of returning an error?
|
169
|
-
relation = @model.public_send(
|
170
|
+
relation = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_value).first
|
170
171
|
if relation.nil?
|
171
|
-
@model.public_send(
|
172
|
+
@model.public_send(relation_name) << related_resource.model
|
172
173
|
else
|
173
174
|
fail JSONAPI::Exceptions::HasManyRelationExists.new(relationship_key_value)
|
174
175
|
end
|
@@ -544,9 +545,50 @@ module JSONAPI
|
|
544
545
|
end
|
545
546
|
end
|
546
547
|
|
547
|
-
|
548
|
-
|
549
|
-
|
548
|
+
def key_type(key_type)
|
549
|
+
@_resource_key_type = key_type
|
550
|
+
end
|
551
|
+
|
552
|
+
def resource_key_type
|
553
|
+
@_resource_key_type || JSONAPI.configuration.resource_key_type
|
554
|
+
end
|
555
|
+
|
556
|
+
def verify_key(key, context = nil)
|
557
|
+
key_type = resource_key_type
|
558
|
+
verification_proc = case key_type
|
559
|
+
|
560
|
+
when :integer
|
561
|
+
-> (key, context) {
|
562
|
+
begin
|
563
|
+
return key if key.nil?
|
564
|
+
Integer(key)
|
565
|
+
rescue
|
566
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
567
|
+
end
|
568
|
+
}
|
569
|
+
when :string
|
570
|
+
-> (key, context) {
|
571
|
+
return key if key.nil?
|
572
|
+
if key.to_s.include?(',')
|
573
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
574
|
+
else
|
575
|
+
key
|
576
|
+
end
|
577
|
+
}
|
578
|
+
when :uuid
|
579
|
+
-> (key, context) {
|
580
|
+
return key if key.nil?
|
581
|
+
if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
|
582
|
+
key
|
583
|
+
else
|
584
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
585
|
+
end
|
586
|
+
}
|
587
|
+
else
|
588
|
+
key_type
|
589
|
+
end
|
590
|
+
|
591
|
+
verification_proc.call(key, context)
|
550
592
|
rescue
|
551
593
|
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
552
594
|
end
|
data/lib/jsonapi/routing_ext.rb
CHANGED
@@ -68,6 +68,10 @@ module ActionDispatch
|
|
68
68
|
|
69
69
|
options[:path] = format_route(@resource_type)
|
70
70
|
|
71
|
+
if res.resource_key_type == :uuid
|
72
|
+
options[:constraints] = {id: /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(,[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})*/}
|
73
|
+
end
|
74
|
+
|
71
75
|
if options[:except]
|
72
76
|
options[:except] = Array(options[:except])
|
73
77
|
options[:except] << :new unless options[:except].include?(:new) || options[:except].include?('new')
|
@@ -397,8 +397,11 @@ end
|
|
397
397
|
class PurchaseOrder < ActiveRecord::Base
|
398
398
|
belongs_to :customer
|
399
399
|
has_many :line_items
|
400
|
+
has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id'
|
400
401
|
|
401
402
|
has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags
|
403
|
+
|
404
|
+
has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag'
|
402
405
|
end
|
403
406
|
|
404
407
|
class OrderFlag < ActiveRecord::Base
|
@@ -618,6 +621,9 @@ module Api
|
|
618
621
|
end
|
619
622
|
|
620
623
|
class PurchaseOrdersController < JSONAPI::ResourceController
|
624
|
+
def context
|
625
|
+
{current_user: $test_user}
|
626
|
+
end
|
621
627
|
end
|
622
628
|
|
623
629
|
class LineItemsController < JSONAPI::ResourceController
|
@@ -794,16 +800,8 @@ class PostResource < JSONAPI::Resource
|
|
794
800
|
return filter, values
|
795
801
|
end
|
796
802
|
|
797
|
-
def self.is_num?(str)
|
798
|
-
begin
|
799
|
-
!!Integer(str)
|
800
|
-
rescue ArgumentError, TypeError
|
801
|
-
false
|
802
|
-
end
|
803
|
-
end
|
804
|
-
|
805
803
|
def self.verify_key(key, context = nil)
|
806
|
-
|
804
|
+
super(key)
|
807
805
|
raise JSONAPI::Exceptions::RecordNotFound.new(key) unless find_by_key(key, context: context)
|
808
806
|
return key
|
809
807
|
end
|
@@ -819,9 +817,7 @@ class IsoCurrencyResource < JSONAPI::Resource
|
|
819
817
|
|
820
818
|
filter :country_name
|
821
819
|
|
822
|
-
|
823
|
-
key && String(key)
|
824
|
-
end
|
820
|
+
key_type :string
|
825
821
|
end
|
826
822
|
|
827
823
|
class ExpenseEntryResource < JSONAPI::Resource
|
@@ -1202,8 +1198,28 @@ module Api
|
|
1202
1198
|
attribute :total
|
1203
1199
|
|
1204
1200
|
has_one :customer
|
1205
|
-
has_many :line_items
|
1206
|
-
|
1201
|
+
has_many :line_items, relation_name: -> (options = {}) {
|
1202
|
+
context = options[:context]
|
1203
|
+
current_user = context ? context[:current_user] : nil
|
1204
|
+
|
1205
|
+
unless current_user && current_user.book_admin
|
1206
|
+
:line_items
|
1207
|
+
else
|
1208
|
+
:admin_line_items
|
1209
|
+
end
|
1210
|
+
}
|
1211
|
+
|
1212
|
+
has_many :order_flags, acts_as_set: true,
|
1213
|
+
relation_name: -> (options = {}) {
|
1214
|
+
context = options[:context]
|
1215
|
+
current_user = context ? context[:current_user] : nil
|
1216
|
+
|
1217
|
+
unless current_user && current_user.book_admin
|
1218
|
+
:order_flags
|
1219
|
+
else
|
1220
|
+
:admin_order_flags
|
1221
|
+
end
|
1222
|
+
}
|
1207
1223
|
end
|
1208
1224
|
|
1209
1225
|
class OrderFlagResource < JSONAPI::Resource
|
data/test/fixtures/customers.yml
CHANGED
@@ -426,6 +426,16 @@ class RequestTest < ActionDispatch::IntegrationTest
|
|
426
426
|
JSONAPI.configuration.top_level_meta_include_record_count = false
|
427
427
|
end
|
428
428
|
|
429
|
+
def test_filter_related_resources
|
430
|
+
JSONAPI.configuration.top_level_meta_include_record_count = true
|
431
|
+
get '/api/v2/books/1/book_comments?filter[book]=2'
|
432
|
+
assert_equal 0, json_response['meta']['record_count']
|
433
|
+
get '/api/v2/books/1/book_comments?filter[book]=1&page[limit]=20'
|
434
|
+
assert_equal 26, json_response['meta']['record_count']
|
435
|
+
ensure
|
436
|
+
JSONAPI.configuration.top_level_meta_include_record_count = false
|
437
|
+
end
|
438
|
+
|
429
439
|
def test_pagination_related_resources_without_related
|
430
440
|
Api::V2::BookResource.paginator :offset
|
431
441
|
Api::V2::BookCommentResource.paginator :offset
|
@@ -750,6 +760,40 @@ class RequestTest < ActionDispatch::IntegrationTest
|
|
750
760
|
JSONAPI.configuration = original_config
|
751
761
|
end
|
752
762
|
|
763
|
+
def test_patch_formatted_dasherized_replace_to_many_computed_relation
|
764
|
+
$original_test_user = $test_user
|
765
|
+
$test_user = Person.find(5)
|
766
|
+
original_config = JSONAPI.configuration.dup
|
767
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
768
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
769
|
+
patch '/api/v6/purchase-orders/2?include=line-items,order-flags',
|
770
|
+
{
|
771
|
+
'data' => {
|
772
|
+
'id' => '2',
|
773
|
+
'type' => 'purchase-orders',
|
774
|
+
'relationships' => {
|
775
|
+
'line-items' => {
|
776
|
+
'data' => [
|
777
|
+
{'type' => 'line-items', 'id' => '3'},
|
778
|
+
{'type' => 'line-items', 'id' => '4'}
|
779
|
+
]
|
780
|
+
},
|
781
|
+
'order-flags' => {
|
782
|
+
'data' => [
|
783
|
+
{'type' => 'order-flags', 'id' => '1'},
|
784
|
+
{'type' => 'order-flags', 'id' => '2'}
|
785
|
+
]
|
786
|
+
}
|
787
|
+
}
|
788
|
+
}
|
789
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
790
|
+
|
791
|
+
assert_equal 200, status
|
792
|
+
ensure
|
793
|
+
JSONAPI.configuration = original_config
|
794
|
+
$test_user = $original_test_user
|
795
|
+
end
|
796
|
+
|
753
797
|
def test_post_to_many_link
|
754
798
|
original_config = JSONAPI.configuration.dup
|
755
799
|
JSONAPI.configuration.route_format = :dasherized_route
|
@@ -767,6 +811,26 @@ class RequestTest < ActionDispatch::IntegrationTest
|
|
767
811
|
JSONAPI.configuration = original_config
|
768
812
|
end
|
769
813
|
|
814
|
+
def test_post_computed_relation_to_many
|
815
|
+
$original_test_user = $test_user
|
816
|
+
$test_user = Person.find(5)
|
817
|
+
original_config = JSONAPI.configuration.dup
|
818
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
819
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
820
|
+
post '/api/v6/purchase-orders/4/relationships/line-items',
|
821
|
+
{
|
822
|
+
'data' => [
|
823
|
+
{'type' => 'line-items', 'id' => '5'},
|
824
|
+
{'type' => 'line-items', 'id' => '6'}
|
825
|
+
]
|
826
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
827
|
+
|
828
|
+
assert_equal 204, status
|
829
|
+
ensure
|
830
|
+
JSONAPI.configuration = original_config
|
831
|
+
$test_user = $original_test_user
|
832
|
+
end
|
833
|
+
|
770
834
|
def test_patch_to_many_link
|
771
835
|
original_config = JSONAPI.configuration.dup
|
772
836
|
JSONAPI.configuration.route_format = :dasherized_route
|
@@ -784,6 +848,26 @@ class RequestTest < ActionDispatch::IntegrationTest
|
|
784
848
|
JSONAPI.configuration = original_config
|
785
849
|
end
|
786
850
|
|
851
|
+
def test_patch_to_many_link_computed_relation
|
852
|
+
$original_test_user = $test_user
|
853
|
+
$test_user = Person.find(5)
|
854
|
+
original_config = JSONAPI.configuration.dup
|
855
|
+
JSONAPI.configuration.route_format = :dasherized_route
|
856
|
+
JSONAPI.configuration.json_key_format = :dasherized_key
|
857
|
+
patch '/api/v6/purchase-orders/4/relationships/order-flags',
|
858
|
+
{
|
859
|
+
'data' => [
|
860
|
+
{'type' => 'order-flags', 'id' => '1'},
|
861
|
+
{'type' => 'order-flags', 'id' => '2'}
|
862
|
+
]
|
863
|
+
}.to_json, "CONTENT_TYPE" => JSONAPI::MEDIA_TYPE
|
864
|
+
|
865
|
+
assert_equal 204, status
|
866
|
+
ensure
|
867
|
+
JSONAPI.configuration = original_config
|
868
|
+
$test_user = $original_test_user
|
869
|
+
end
|
870
|
+
|
787
871
|
def test_patch_to_one
|
788
872
|
original_config = JSONAPI.configuration.dup
|
789
873
|
JSONAPI.configuration.route_format = :dasherized_route
|
@@ -52,6 +52,17 @@ class RoutesTest < ActionDispatch::IntegrationTest
|
|
52
52
|
{controller: 'posts', action: 'update_relationship', post_id: '1', relationship: 'tags'})
|
53
53
|
end
|
54
54
|
|
55
|
+
def test_routing_uuid
|
56
|
+
assert_routing({path: '/pets/v1/cats/f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5', method: :get},
|
57
|
+
{action: 'show', controller: 'pets/v1/cats', id: 'f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5'})
|
58
|
+
end
|
59
|
+
|
60
|
+
# ToDo: refute this routing
|
61
|
+
# def test_routing_uuid_bad_format
|
62
|
+
# assert_routing({path: '/pets/v1/cats/f1a4d5f2-e77a-4d0a-acbb-ee0b9', method: :get},
|
63
|
+
# {action: 'show', controller: 'pets/v1/cats', id: 'f1a4d5f2-e77a-4d0a-acbb-ee0b98'})
|
64
|
+
# end
|
65
|
+
|
55
66
|
# Polymorphic
|
56
67
|
def test_routing_polymorphic_get_related_resource
|
57
68
|
assert_routing(
|
@@ -17,6 +17,11 @@ module Jsonapi
|
|
17
17
|
assert_file 'app/resources/post_resource.rb', /class PostResource < JSONAPI::Resource/
|
18
18
|
end
|
19
19
|
|
20
|
+
test "resource is singular" do
|
21
|
+
run_generator ["posts"]
|
22
|
+
assert_file 'app/resources/post_resource.rb', /class PostResource < JSONAPI::Resource/
|
23
|
+
end
|
24
|
+
|
20
25
|
test "resource is created with namespace" do
|
21
26
|
run_generator ["api/v1/post"]
|
22
27
|
assert_file 'app/resources/api/v1/post_resource.rb', /class Api::V1::PostResource < JSONAPI::Resource/
|
data/test/test_helper.rb
CHANGED
@@ -95,6 +95,22 @@ TestApp.initialize!
|
|
95
95
|
|
96
96
|
require File.expand_path('../fixtures/active_record', __FILE__)
|
97
97
|
|
98
|
+
module Pets
|
99
|
+
module V1
|
100
|
+
class CatsController < JSONAPI::ResourceController
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
class CatResource < JSONAPI::Resource
|
105
|
+
attribute :id
|
106
|
+
attribute :name
|
107
|
+
attribute :breed
|
108
|
+
|
109
|
+
key_type :uuid
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
98
114
|
JSONAPI.configuration.route_format = :underscored_route
|
99
115
|
TestApp.routes.draw do
|
100
116
|
jsonapi_resources :people
|
@@ -214,6 +230,12 @@ TestApp.routes.draw do
|
|
214
230
|
end
|
215
231
|
end
|
216
232
|
|
233
|
+
namespace :pets do
|
234
|
+
namespace :v1 do
|
235
|
+
jsonapi_resources :cats
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
217
239
|
mount MyEngine::Engine => "/boomshaka", as: :my_engine
|
218
240
|
end
|
219
241
|
|
@@ -195,6 +195,7 @@ class ResourceTest < ActiveSupport::TestCase
|
|
195
195
|
filtered_comments = post_resource.comments({ filters: { body: 'i liked it' } })
|
196
196
|
assert_equal(1, filtered_comments.size)
|
197
197
|
|
198
|
+
ensure
|
198
199
|
# reset method to original implementation
|
199
200
|
PostResource.instance_eval do
|
200
201
|
def apply_filters(records, filters, options)
|
@@ -222,6 +223,7 @@ class ResourceTest < ActiveSupport::TestCase
|
|
222
223
|
sorted_comment_ids = post_resource.comments(sort_criteria: [{ field: 'id', direction: 'desc'}]).map{|c| c.model.id }
|
223
224
|
assert_equal [2,1], sorted_comment_ids
|
224
225
|
|
226
|
+
ensure
|
225
227
|
# reset method to original implementation
|
226
228
|
PostResource.instance_eval do
|
227
229
|
def apply_sort(records, criteria)
|
@@ -260,6 +262,7 @@ class ResourceTest < ActiveSupport::TestCase
|
|
260
262
|
paged_comments = post_resource.comments(paginator: paginator_class.new(1))
|
261
263
|
assert_equal 1, paged_comments.size
|
262
264
|
|
265
|
+
ensure
|
263
266
|
# reset method to original implementation
|
264
267
|
PostResource.instance_eval do
|
265
268
|
def apply_pagination(records, criteria, order_options)
|
@@ -269,4 +272,81 @@ class ResourceTest < ActiveSupport::TestCase
|
|
269
272
|
end
|
270
273
|
end
|
271
274
|
end
|
275
|
+
|
276
|
+
def test_key_type_integer
|
277
|
+
CatResource.instance_eval do
|
278
|
+
key_type :integer
|
279
|
+
end
|
280
|
+
|
281
|
+
assert CatResource.verify_key('45')
|
282
|
+
assert CatResource.verify_key(45)
|
283
|
+
|
284
|
+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
|
285
|
+
CatResource.verify_key('45,345')
|
286
|
+
end
|
287
|
+
|
288
|
+
ensure
|
289
|
+
CatResource.instance_eval do
|
290
|
+
key_type nil
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def test_key_type_string
|
295
|
+
CatResource.instance_eval do
|
296
|
+
key_type :string
|
297
|
+
end
|
298
|
+
|
299
|
+
assert CatResource.verify_key('45')
|
300
|
+
assert CatResource.verify_key(45)
|
301
|
+
|
302
|
+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
|
303
|
+
CatResource.verify_key('45,345')
|
304
|
+
end
|
305
|
+
|
306
|
+
ensure
|
307
|
+
CatResource.instance_eval do
|
308
|
+
key_type nil
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def test_key_type_uuid
|
313
|
+
CatResource.instance_eval do
|
314
|
+
key_type :uuid
|
315
|
+
end
|
316
|
+
|
317
|
+
assert CatResource.verify_key('f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5')
|
318
|
+
|
319
|
+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
|
320
|
+
CatResource.verify_key('f1a-e77a-4d0a-acbb-ee0b98b3f6b5')
|
321
|
+
end
|
322
|
+
|
323
|
+
ensure
|
324
|
+
CatResource.instance_eval do
|
325
|
+
key_type nil
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def test_key_type_proc
|
330
|
+
CatResource.instance_eval do
|
331
|
+
key_type -> (key, context) {
|
332
|
+
return key if key.nil?
|
333
|
+
if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
|
334
|
+
key
|
335
|
+
else
|
336
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
337
|
+
end
|
338
|
+
}
|
339
|
+
end
|
340
|
+
|
341
|
+
assert CatResource.verify_key('f1a4d5f2-e77a-4d0a-acbb-ee0b98b3f6b5')
|
342
|
+
|
343
|
+
assert_raises JSONAPI::Exceptions::InvalidFieldValue do
|
344
|
+
CatResource.verify_key('f1a-e77a-4d0a-acbb-ee0b98b3f6b5')
|
345
|
+
end
|
346
|
+
|
347
|
+
ensure
|
348
|
+
CatResource.instance_eval do
|
349
|
+
key_type nil
|
350
|
+
end
|
351
|
+
end
|
272
352
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi-resources
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Gebhardt
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-09-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|