jsonapi-resources 0.5.7 → 0.5.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|