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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab4c4a0d0d8f176e2085aee265a0d0678876c8bd
4
- data.tar.gz: 04ea25473f89a0bf77df0778b2ebaa7d4dd3aa8d
3
+ metadata.gz: 61e3e8cacf54569eb7d57771d08cdd4861a84a22
4
+ data.tar.gz: 25ccc4dc6996b991af26ebc7598129615ee14b4c
5
5
  SHA512:
6
- metadata.gz: b19b8f700405d1caf6d676a1d984d8d2e0a82aa3d0df31b140d26484f318e74fc94311b4744e40d16ca7bf900a64a27a86351fe588bdee413569da5bbc299c63
7
- data.tar.gz: 09b9a7ac8901f2b3c6ff218b8680c8cc313d806cfbd472989623f94871d95466375b6929cbfb2d48105799598a2fdeeee67fd27915fc054e1f53ea191a469d55
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
- rails = case version
15
- when 'master'
16
- { github: 'rails/rails' }
17
- when 'default'
18
- '>= 4.2'
19
- else
20
- "~> #{version}"
21
- end
22
- gem 'rails', 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 override
211
- `verify_key` class method:
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
- class CurrencyResource < JSONAPI::Resource
215
- primary_key :code
216
- attributes :code, :name
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
- has_many :expense_entries
244
+ ##### Override key type on a resource
219
245
 
220
- def self.verify_key(key, context = nil)
221
- key && String(key)
222
- end
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 = :true
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
@@ -6,7 +6,7 @@ module Jsonapi
6
6
  template_file = File.join(
7
7
  'app/resources',
8
8
  class_path,
9
- "#{file_name}_resource.rb"
9
+ "#{file_name.singularize}_resource.rb"
10
10
  )
11
11
  template 'jsonapi_resource.rb', template_file
12
12
  end
@@ -1,4 +1,4 @@
1
1
  <% module_namespacing do -%>
2
- class <%= class_name %>Resource < JSONAPI::Resource
2
+ class <%= class_name.singularize %>Resource < JSONAPI::Resource
3
3
  end
4
4
  <% end -%>
@@ -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)
@@ -14,7 +14,8 @@ module JSONAPI
14
14
  end
15
15
  @source = options[:source]
16
16
  @links = options[:links]
17
- @status = options[:status]
17
+
18
+ @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[options[:status]].to_s
18
19
  end
19
20
  end
20
21
 
@@ -11,7 +11,7 @@ module JSONAPI
11
11
 
12
12
  def errors
13
13
  [JSONAPI::Error.new(code: JSONAPI::INTERNAL_SERVER_ERROR,
14
- status: 500,
14
+ status: :internal_server_error,
15
15
  title: 'Internal Server Error',
16
16
  detail: 'Internal Server Error')]
17
17
  end
@@ -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
 
@@ -24,7 +24,7 @@ module JSONAPI
24
24
  @resource_klass ||= Resource.resource_for(@module_path + @class_name)
25
25
  end
26
26
 
27
- def relation_name(options = {})
27
+ def relation_name(options)
28
28
  case @relation_name
29
29
  when Symbol
30
30
  # :nocov:
@@ -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
- unless links_object[:id].nil?
422
- relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(links_object[:type]).to_s)
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
- if link_value.is_a?(Array) && link_value.length == 0
434
- linkage = []
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)
@@ -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(relationship.type).where(relationship.primary_key => relationship_key_value).first
170
+ relation = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_value).first
170
171
  if relation.nil?
171
- @model.public_send(relationship.type) << related_resource.model
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
- # override to allow for key processing and checking
548
- def verify_key(key, _context = nil)
549
- key && Integer(key)
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
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.5.7'
3
+ VERSION = '0.5.8'
4
4
  end
5
5
  end
@@ -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
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) unless is_num?(key)
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
- def self.verify_key(key, context = nil)
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
- has_many :order_flags, acts_as_set: true
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
@@ -5,3 +5,7 @@ xyz_corp:
5
5
  abc_corp:
6
6
  id: 2
7
7
  name: ABC Corporation
8
+
9
+ asdfg_corp:
10
+ id: 3
11
+ name: ASDFG Corporation
@@ -28,4 +28,10 @@ li_5:
28
28
  id: 5
29
29
  part_number: 5WERT
30
30
  quantity: 1
31
- item_cost: 299.98
31
+ item_cost: 299.98
32
+
33
+ li_6:
34
+ id: 6
35
+ part_number: 25washer
36
+ quantity: 10
37
+ item_cost: 0.98
@@ -15,3 +15,9 @@ po_3:
15
15
  requested_delivery_date:
16
16
  delivery_date: nil
17
17
  customer_id: 1
18
+
19
+ po_4:
20
+ id: 4
21
+ requested_delivery_date:
22
+ delivery_date: nil
23
+ customer_id: 3
@@ -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/
@@ -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.7
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-08-13 00:00:00.000000000 Z
12
+ date: 2015-09-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler