jsonapi-resources 0.7.0 → 0.7.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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