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 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