jsonapi-resources 0.7.0 → 0.7.1.beta1

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.
@@ -25,6 +25,10 @@ module JSONAPI
25
25
  @resource_klass = @parent_resource.resource_for(@class_name)
26
26
  end
27
27
 
28
+ def table_name
29
+ @table_name ||= resource_klass._table_name
30
+ end
31
+
28
32
  def relation_name(options)
29
33
  case @relation_name
30
34
  when Symbol
@@ -47,6 +51,10 @@ module JSONAPI
47
51
  end
48
52
  end
49
53
 
54
+ def belongs_to?
55
+ false
56
+ end
57
+
50
58
  class ToOne < Relationship
51
59
  attr_reader :foreign_key_on
52
60
 
@@ -32,11 +32,9 @@ module JSONAPI
32
32
 
33
33
  @resource_klass ||= Resource.resource_for(params[:controller]) if params[:controller]
34
34
 
35
- unless params.nil?
36
- setup_action_method_name = "setup_#{params[:action]}_action"
37
- if respond_to?(setup_action_method_name)
38
- send(setup_action_method_name, params)
39
- end
35
+ setup_action_method_name = "setup_#{params[:action]}_action"
36
+ if respond_to?(setup_action_method_name)
37
+ send(setup_action_method_name, params)
40
38
  end
41
39
  rescue ActionController::ParameterMissing => e
42
40
  @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
@@ -516,7 +514,7 @@ module JSONAPI
516
514
  params.each do |key, value|
517
515
  case key.to_s
518
516
  when 'relationships'
519
- value.each_key do |links_key|
517
+ value.keys.each do |links_key|
520
518
  unless formatted_allowed_fields.include?(links_key.to_sym)
521
519
  params_not_allowed.push(links_key)
522
520
  unless JSONAPI.configuration.raise_if_parameters_not_allowed
@@ -105,8 +105,9 @@ module JSONAPI
105
105
  end
106
106
  end
107
107
 
108
+ # Override this on a resource instance to override the fetchable keys
108
109
  def fetchable_fields
109
- self.class.fetchable_fields(context)
110
+ self.class.fields
110
111
  end
111
112
 
112
113
  # Override this on a resource to customize how the associated records
@@ -119,6 +120,29 @@ module JSONAPI
119
120
  _model.errors.messages
120
121
  end
121
122
 
123
+ # Add metadata to validation error objects.
124
+ #
125
+ # Suppose `model_error_messages` returned the following error messages
126
+ # hash:
127
+ #
128
+ # {password: ["too_short", "format"]}
129
+ #
130
+ # Then to add data to the validation error `validation_error_metadata`
131
+ # could return:
132
+ #
133
+ # {
134
+ # password: {
135
+ # "too_short": {"minimum_length" => 6},
136
+ # "format": {"requirement" => "must contain letters and numbers"}
137
+ # }
138
+ # }
139
+ #
140
+ # The specified metadata is then be merged into the validation error
141
+ # object.
142
+ def validation_error_metadata
143
+ {}
144
+ end
145
+
122
146
  # Override this to return resource level meta data
123
147
  # must return a hash, and if the hash is empty the meta section will not be serialized with the resource
124
148
  # meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
@@ -173,8 +197,9 @@ module JSONAPI
173
197
  end
174
198
 
175
199
  def _remove
176
- @model.destroy
177
-
200
+ unless @model.destroy
201
+ fail JSONAPI::Exceptions::ValidationErrors.new(self)
202
+ end
178
203
  :completed
179
204
  end
180
205
 
@@ -440,11 +465,6 @@ module JSONAPI
440
465
  end
441
466
  # :nocov:
442
467
 
443
- # Override in your resource to filter the fetchable keys
444
- def fetchable_fields(_context = nil)
445
- fields
446
- end
447
-
448
468
  # Override in your resource to filter the updatable keys
449
469
  def updatable_fields(_context = nil)
450
470
  _updatable_relationships | _attributes.keys - [:id]
@@ -523,11 +543,11 @@ module JSONAPI
523
543
  if filters
524
544
  filters.each do |filter, value|
525
545
  if _relationships.include?(filter)
526
- if _relationships[filter].is_a?(JSONAPI::Relationship::ToMany)
527
- required_includes.push(filter.to_s)
528
- records = apply_filter(records, "#{filter}.#{_relationships[filter].primary_key}", value, options)
546
+ if _relationships[filter].belongs_to?
547
+ records = apply_filter(records, _relationships[filter].foreign_key, value, options)
529
548
  else
530
- records = apply_filter(records, "#{_relationships[filter].foreign_key}", value, options)
549
+ required_includes.push(filter.to_s)
550
+ records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
531
551
  end
532
552
  else
533
553
  records = apply_filter(records, filter, value, options)
@@ -587,7 +607,7 @@ module JSONAPI
587
607
  # Override this method if you want to customize the relation for
588
608
  # finder methods (find, find_by_key)
589
609
  def records(_options = {})
590
- _model_class
610
+ _model_class.all
591
611
  end
592
612
 
593
613
  def verify_filters(filters, context = nil)
@@ -696,6 +716,10 @@ module JSONAPI
696
716
  @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
697
717
  end
698
718
 
719
+ def _table_name
720
+ @_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
721
+ end
722
+
699
723
  def _as_parent_key
700
724
  @_as_parent_key ||= "#{_type.to_s.singularize}_id"
701
725
  end
@@ -1,5 +1,17 @@
1
1
  module JSONAPI
2
- class ResourceController < ActionController::Base
3
- include JSONAPI::ActsAsResourceController
2
+ class ResourceController < ActionController::Metal
3
+ MODULES = [
4
+ AbstractController::Rendering,
5
+ ActionController::Rendering,
6
+ ActionController::Renderers::All,
7
+ ActionController::StrongParameters,
8
+ ActionController::ForceSSL,
9
+ ActionController::Instrumentation,
10
+ JSONAPI::ActsAsResourceController
11
+ ].freeze
12
+
13
+ MODULES.each do |mod|
14
+ include mod
15
+ end
4
16
  end
5
17
  end
@@ -93,14 +93,7 @@ module JSONAPI
93
93
  # The fields options controls both fields and included links references.
94
94
  def process_primary(source, include_directives)
95
95
  if source.respond_to?(:to_ary)
96
- source.each do |resource|
97
- id = resource.id
98
- if already_serialized?(resource.class._type, id)
99
- set_primary(@primary_class_name, id)
100
- end
101
-
102
- add_included_object(id, object_hash(resource, include_directives), true)
103
- end
96
+ source.each { |resource| process_primary(resource, include_directives) }
104
97
  else
105
98
  return {} if source.nil?
106
99
 
@@ -278,6 +271,7 @@ module JSONAPI
278
271
  end
279
272
 
280
273
  def link_object_to_many(source, relationship, include_linkage)
274
+ include_linkage = include_linkage | relationship.always_include_linkage_data
281
275
  link_object_hash = {}
282
276
  link_object_hash[:links] = {}
283
277
  link_object_hash[:links][:self] = self_link(source, relationship)
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.7.0'
3
+ VERSION = '0.7.1.beta1'
4
4
  end
5
5
  end
@@ -69,7 +69,7 @@ module ActionDispatch
69
69
  options[:path] = format_route(@resource_type)
70
70
 
71
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})*/}
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}/}
73
73
  end
74
74
 
75
75
  if options[:except]
@@ -0,0 +1,80 @@
1
+ en:
2
+ jsonapi-resources:
3
+ exceptions:
4
+ internal_server_error:
5
+ title: 'Internal Server Error'
6
+ detail: 'Internal Server Error'
7
+ invalid_resource:
8
+ title: 'Invalid resource'
9
+ detail: "%{resource} is not a valid resource."
10
+ record_not_found:
11
+ title: 'Record not found'
12
+ detail: "The record identified by %{id} could not be found."
13
+ unsupported_media_type:
14
+ title: 'Unsupported media type'
15
+ detail: "All requests that create or update must use the '%{needed_media_type}' Content-Type. This request specified '%{media_type}.'"
16
+ has_many_relation:
17
+ title: 'Relation exists'
18
+ detail: "The relation to %{id} already exists."
19
+ to_many_set_replacement_forbidden:
20
+ title: 'Complete replacement forbidden'
21
+ detail: 'Complete replacement forbidden for this relationship'
22
+ invalid_filter_syntax:
23
+ title: 'Invalid filters syntax'
24
+ detail: "%{filters} is not a valid syntax for filtering."
25
+ filter_not_allowed:
26
+ title: "Filter not allowed"
27
+ detail: "%{filter} is not allowed."
28
+ invalid_filter_value:
29
+ title: 'Invalid filter value'
30
+ detail: "%{value} is not a valid value for %{filter}."
31
+ invalid_field_value:
32
+ title: 'Invalid field value'
33
+ detail: "%{value} is not a valid value for %{field}."
34
+ invalid_field_format:
35
+ title: 'Invalid field format'
36
+ detail: 'Fields must specify a type.'
37
+ invalid_links_object:
38
+ title: 'Invalid Links Object'
39
+ detail: 'Data is not a valid Links Object.'
40
+ type_mismatch:
41
+ title: 'Type Mismatch'
42
+ detail: "%{type} is not a valid type for this operation."
43
+ invalid_field:
44
+ title: 'Invalid field'
45
+ detail: "%{field} is not a valid field for %{type}."
46
+ invalid_include:
47
+ title: 'Invalid field'
48
+ detail: "%{relationship} is not a valid relationship of %{resource}"
49
+ invalid_sort_criteria:
50
+ title: 'Invalid sort criteria'
51
+ detail: "%{sort_criteria} is not a valid sort criteria for %{resource}"
52
+ parameters_not_allowed:
53
+ title: 'Param not allowed'
54
+ detail: "%{param} is not allowed."
55
+ parameter_missing:
56
+ title: 'Missing Parameter'
57
+ detail: "The required parameter, %{param}, is missing."
58
+ count_mismatch:
59
+ title: 'Count to key mismatch'
60
+ detail: 'The resource collection does not contain the same number of objects as the number of keys.'
61
+ key_not_included_in_url:
62
+ title: 'Key is not included in URL'
63
+ detail: "The URL does not support the key %{key}"
64
+ missing_key:
65
+ title: 'A key is required'
66
+ detail: 'The resource object does not contain a key.'
67
+ record_locked:
68
+ title: 'Locked resource'
69
+ save_failed:
70
+ title: 'Save failed or was cancelled'
71
+ detail: 'Save failed or was cancelled'
72
+ invalid_page_object:
73
+ title: 'Invalid Page Object'
74
+ detail: 'Invalid Page Object.'
75
+ page_parameters_not_allowed:
76
+ title: 'Page parameter not allowed'
77
+ detail: "%{param} is not an allowed page parameter."
78
+ invalid_page_value:
79
+ title: 'Invalid page value'
80
+ detail: "%{value} is not a valid value for %{page} page parameter."
@@ -485,7 +485,7 @@ class PostsControllerTest < ActionController::TestCase
485
485
  assert_equal 1, json_response['meta']["warnings"].count
486
486
  assert_equal "Param not allowed", json_response['meta']["warnings"][0]["title"]
487
487
  assert_equal "asdfg is not allowed.", json_response['meta']["warnings"][0]["detail"]
488
- assert_equal 105, json_response['meta']["warnings"][0]["code"]
488
+ assert_equal '105', json_response['meta']["warnings"][0]["code"]
489
489
  ensure
490
490
  JSONAPI.configuration.raise_if_parameters_not_allowed = true
491
491
  end
@@ -695,7 +695,7 @@ class PostsControllerTest < ActionController::TestCase
695
695
  assert_equal 1, json_response['meta']["warnings"].count
696
696
  assert_equal "Param not allowed", json_response['meta']["warnings"][0]["title"]
697
697
  assert_equal "subject is not allowed.", json_response['meta']["warnings"][0]["detail"]
698
- assert_equal 105, json_response['meta']["warnings"][0]["code"]
698
+ assert_equal '105', json_response['meta']["warnings"][0]["code"]
699
699
  ensure
700
700
  JSONAPI.configuration.raise_if_parameters_not_allowed = true
701
701
  end
@@ -866,7 +866,7 @@ class PostsControllerTest < ActionController::TestCase
866
866
  assert_equal 1, json_response['meta']["warnings"].count
867
867
  assert_equal "Param not allowed", json_response['meta']["warnings"][0]["title"]
868
868
  assert_equal "subject is not allowed.", json_response['meta']["warnings"][0]["detail"]
869
- assert_equal 105, json_response['meta']["warnings"][0]["code"]
869
+ assert_equal '105', json_response['meta']["warnings"][0]["code"]
870
870
  ensure
871
871
  JSONAPI.configuration.raise_if_parameters_not_allowed = true
872
872
  end
@@ -1653,6 +1653,14 @@ class PostsControllerTest < ActionController::TestCase
1653
1653
  assert_response :bad_request
1654
1654
  end
1655
1655
 
1656
+ def test_delete_with_validation_error
1657
+ post = Post.create!(title: "can't destroy me", author: Person.first)
1658
+ delete :destroy, { id: post.id }
1659
+
1660
+ assert_equal "can't destroy me", json_response['errors'][0]['title']
1661
+ assert_response :unprocessable_entity
1662
+ end
1663
+
1656
1664
  def test_delete_single
1657
1665
  initial_count = Post.count
1658
1666
  delete :destroy, {id: '4'}
@@ -3273,10 +3281,32 @@ class Api::V7::CustomersControllerTest < ActionController::TestCase
3273
3281
  end
3274
3282
 
3275
3283
  class Api::V7::CategoriesControllerTest < ActionController::TestCase
3276
- def test_uncaught_error_in_controller
3284
+ def test_uncaught_error_in_controller_translated_to_internal_server_error
3277
3285
 
3278
3286
  get :show, {id: '1'}
3279
3287
  assert_response 500
3280
3288
  assert_match /Internal Server Error/, json_response['errors'][0]['detail']
3281
3289
  end
3282
- end
3290
+
3291
+ def test_not_whitelisted_error_in_controller
3292
+ original_config = JSONAPI.configuration.dup
3293
+ JSONAPI.configuration.operations_processor = :error_raising
3294
+ JSONAPI.configuration.exception_class_whitelist = []
3295
+ get :show, {id: '1'}
3296
+ assert_response 500
3297
+ assert_match /Internal Server Error/, json_response['errors'][0]['detail']
3298
+ ensure
3299
+ JSONAPI.configuration = original_config
3300
+ end
3301
+
3302
+ def test_whitelisted_error_in_controller
3303
+ original_config = JSONAPI.configuration.dup
3304
+ JSONAPI.configuration.operations_processor = :error_raising
3305
+ JSONAPI.configuration.exception_class_whitelist = [PostsController::SubSpecialError]
3306
+ assert_raises PostsController::SubSpecialError do
3307
+ get :show, {id: '1'}
3308
+ end
3309
+ ensure
3310
+ JSONAPI.configuration = original_config
3311
+ end
3312
+ end
@@ -282,6 +282,15 @@ class Post < ActiveRecord::Base
282
282
 
283
283
  validates :author, presence: true
284
284
  validates :title, length: { maximum: 35 }
285
+
286
+ before_destroy :destroy_callback
287
+
288
+ def destroy_callback
289
+ if title == "can't destroy me"
290
+ errors.add(:title, "can't destroy me")
291
+ return false
292
+ end
293
+ end
285
294
  end
286
295
 
287
296
  class SpecialPostTag < ActiveRecord::Base
@@ -368,8 +377,7 @@ class Crater < ActiveRecord::Base
368
377
  end
369
378
 
370
379
  class Preferences < ActiveRecord::Base
371
- has_one :author, class_name: 'Person'
372
- has_many :friends, class_name: 'Person'
380
+ has_one :author, class_name: 'Person', :inverse_of => 'preferences'
373
381
  end
374
382
 
375
383
  class Fact < ActiveRecord::Base
@@ -979,9 +987,6 @@ class EmployeeResource < JSONAPI::Resource
979
987
  model_name 'Person'
980
988
  end
981
989
 
982
- class FriendResource < JSONAPI::Resource
983
- end
984
-
985
990
  class BreedResource < JSONAPI::Resource
986
991
  attribute :name, format: :title
987
992
 
@@ -1053,8 +1058,7 @@ end
1053
1058
  class PreferencesResource < JSONAPI::Resource
1054
1059
  attribute :advanced_mode
1055
1060
 
1056
- has_one :author, foreign_key: :person_id
1057
- has_many :friends
1061
+ has_one :author, :foreign_key_on => :related
1058
1062
 
1059
1063
  def self.find_by_key(key, options = {})
1060
1064
  new(Preferences.first, nil)
@@ -1164,7 +1168,6 @@ module Api
1164
1168
  class CraterResource < CraterResource; end
1165
1169
  class PreferencesResource < PreferencesResource; end
1166
1170
  class EmployeeResource < EmployeeResource; end
1167
- class FriendResource < FriendResource; end
1168
1171
  class HairCutResource < HairCutResource; end
1169
1172
  class VehicleResource < VehicleResource; end
1170
1173
  class CarResource < CarResource; end
@@ -18,4 +18,4 @@ post_2_thanks_man:
18
18
 
19
19
  rogue_comment:
20
20
  body: Rogue Comment Here
21
- author_id: 3
21
+ author_id: 3
@@ -1,18 +1,14 @@
1
1
  a:
2
2
  id: 1
3
- person_id:
4
3
  advanced_mode: false
5
4
 
6
5
  b:
7
6
  id: 2
8
- person_id:
9
7
  advanced_mode: false
10
8
  c:
11
9
  id: 3
12
- person_id:
13
10
  advanced_mode: false
14
11
 
15
12
  d:
16
13
  id: 4
17
- person_id:
18
14
  advanced_mode: false
@@ -0,0 +1,25 @@
1
+ require File.expand_path('../../../../test_helper', __FILE__)
2
+ require 'generators/jsonapi/controller_generator'
3
+
4
+ module Jsonapi
5
+ class ControllerGeneratorTest < Rails::Generators::TestCase
6
+ tests ControllerGenerator
7
+ destination Rails.root.join('../controllers')
8
+ setup :prepare_destination
9
+ teardown :cleanup_destination_root
10
+
11
+ def cleanup_destination_root
12
+ FileUtils.rm_rf destination_root
13
+ end
14
+
15
+ test "controller is created" do
16
+ run_generator ["post"]
17
+ assert_file 'app/controllers/posts_controller.rb', /class PostsController < JSONAPI::ResourceController/
18
+ end
19
+
20
+ test "controller is created with namespace" do
21
+ run_generator ["api/v1/post"]
22
+ assert_file 'app/controllers/api/v1/posts_controller.rb', /class Api::V1::PostsController < JSONAPI::ResourceController/
23
+ end
24
+ end
25
+ end