jsonapi-resources 0.7.1.beta1 → 0.7.1.beta2

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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +248 -74
  3. data/lib/jsonapi-resources.rb +5 -3
  4. data/lib/jsonapi/acts_as_resource_controller.rb +77 -14
  5. data/lib/jsonapi/configuration.rb +77 -16
  6. data/lib/jsonapi/error.rb +12 -0
  7. data/lib/jsonapi/error_codes.rb +2 -0
  8. data/lib/jsonapi/exceptions.rb +29 -9
  9. data/lib/jsonapi/formatter.rb +29 -4
  10. data/lib/jsonapi/link_builder.rb +18 -18
  11. data/lib/jsonapi/mime_types.rb +25 -6
  12. data/lib/jsonapi/naive_cache.rb +30 -0
  13. data/lib/jsonapi/operation.rb +10 -342
  14. data/lib/jsonapi/operation_dispatcher.rb +87 -0
  15. data/lib/jsonapi/operation_result.rb +2 -1
  16. data/lib/jsonapi/paginator.rb +6 -2
  17. data/lib/jsonapi/processor.rb +283 -0
  18. data/lib/jsonapi/relationship.rb +6 -4
  19. data/lib/jsonapi/{request.rb → request_parser.rb} +46 -35
  20. data/lib/jsonapi/resource.rb +88 -13
  21. data/lib/jsonapi/resource_controller.rb +2 -14
  22. data/lib/jsonapi/resource_controller_metal.rb +17 -0
  23. data/lib/jsonapi/resource_serializer.rb +62 -47
  24. data/lib/jsonapi/resources/version.rb +1 -1
  25. data/lib/jsonapi/response_document.rb +13 -2
  26. data/lib/jsonapi/routing_ext.rb +49 -11
  27. metadata +37 -129
  28. data/.gitignore +0 -22
  29. data/.travis.yml +0 -9
  30. data/Gemfile +0 -23
  31. data/Rakefile +0 -20
  32. data/jsonapi-resources.gemspec +0 -29
  33. data/lib/jsonapi/active_record_operations_processor.rb +0 -35
  34. data/lib/jsonapi/operations_processor.rb +0 -120
  35. data/locales/en.yml +0 -80
  36. data/test/config/database.yml +0 -5
  37. data/test/controllers/controller_test.rb +0 -3312
  38. data/test/fixtures/active_record.rb +0 -1486
  39. data/test/fixtures/author_details.yml +0 -9
  40. data/test/fixtures/book_authors.yml +0 -3
  41. data/test/fixtures/book_comments.yml +0 -12
  42. data/test/fixtures/books.yml +0 -7
  43. data/test/fixtures/categories.yml +0 -35
  44. data/test/fixtures/comments.yml +0 -21
  45. data/test/fixtures/comments_tags.yml +0 -20
  46. data/test/fixtures/companies.yml +0 -4
  47. data/test/fixtures/craters.yml +0 -9
  48. data/test/fixtures/customers.yml +0 -11
  49. data/test/fixtures/documents.yml +0 -3
  50. data/test/fixtures/expense_entries.yml +0 -13
  51. data/test/fixtures/facts.yml +0 -11
  52. data/test/fixtures/hair_cuts.yml +0 -3
  53. data/test/fixtures/iso_currencies.yml +0 -17
  54. data/test/fixtures/line_items.yml +0 -37
  55. data/test/fixtures/makes.yml +0 -2
  56. data/test/fixtures/moons.yml +0 -6
  57. data/test/fixtures/numeros_telefone.yml +0 -3
  58. data/test/fixtures/order_flags.yml +0 -7
  59. data/test/fixtures/people.yml +0 -31
  60. data/test/fixtures/pictures.yml +0 -15
  61. data/test/fixtures/planet_types.yml +0 -19
  62. data/test/fixtures/planets.yml +0 -47
  63. data/test/fixtures/posts.yml +0 -102
  64. data/test/fixtures/posts_tags.yml +0 -59
  65. data/test/fixtures/preferences.yml +0 -14
  66. data/test/fixtures/products.yml +0 -3
  67. data/test/fixtures/purchase_orders.yml +0 -23
  68. data/test/fixtures/sections.yml +0 -8
  69. data/test/fixtures/tags.yml +0 -39
  70. data/test/fixtures/vehicles.yml +0 -17
  71. data/test/fixtures/web_pages.yml +0 -3
  72. data/test/helpers/assertions.rb +0 -13
  73. data/test/helpers/functional_helpers.rb +0 -59
  74. data/test/helpers/value_matchers.rb +0 -60
  75. data/test/helpers/value_matchers_test.rb +0 -40
  76. data/test/integration/requests/namespaced_model_test.rb +0 -13
  77. data/test/integration/requests/request_test.rb +0 -932
  78. data/test/integration/routes/routes_test.rb +0 -218
  79. data/test/integration/sti_fields_test.rb +0 -18
  80. data/test/lib/generators/jsonapi/controller_generator_test.rb +0 -25
  81. data/test/lib/generators/jsonapi/resource_generator_test.rb +0 -30
  82. data/test/test_helper.rb +0 -342
  83. data/test/unit/formatters/dasherized_key_formatter_test.rb +0 -8
  84. data/test/unit/jsonapi_request/jsonapi_request_test.rb +0 -199
  85. data/test/unit/operation/operations_processor_test.rb +0 -528
  86. data/test/unit/pagination/offset_paginator_test.rb +0 -245
  87. data/test/unit/pagination/paged_paginator_test.rb +0 -242
  88. data/test/unit/resource/resource_test.rb +0 -560
  89. data/test/unit/serializer/include_directives_test.rb +0 -113
  90. data/test/unit/serializer/link_builder_test.rb +0 -244
  91. data/test/unit/serializer/polymorphic_serializer_test.rb +0 -383
  92. data/test/unit/serializer/response_document_test.rb +0 -61
  93. data/test/unit/serializer/serializer_test.rb +0 -1939
@@ -1,7 +1,9 @@
1
+ require 'jsonapi/naive_cache'
1
2
  require 'jsonapi/resource'
2
3
  require 'jsonapi/response_document'
3
4
  require 'jsonapi/acts_as_resource_controller'
4
5
  require 'jsonapi/resource_controller'
6
+ require 'jsonapi/resource_controller_metal'
5
7
  require 'jsonapi/resources/version'
6
8
  require 'jsonapi/configuration'
7
9
  require 'jsonapi/paginator'
@@ -12,9 +14,9 @@ require 'jsonapi/resource_serializer'
12
14
  require 'jsonapi/exceptions'
13
15
  require 'jsonapi/error'
14
16
  require 'jsonapi/error_codes'
15
- require 'jsonapi/request'
16
- require 'jsonapi/operations_processor'
17
- require 'jsonapi/active_record_operations_processor'
17
+ require 'jsonapi/request_parser'
18
+ require 'jsonapi/operation_dispatcher'
19
+ require 'jsonapi/processor'
18
20
  require 'jsonapi/relationship'
19
21
  require 'jsonapi/include_directives'
20
22
  require 'jsonapi/operation_result'
@@ -2,11 +2,16 @@ require 'csv'
2
2
 
3
3
  module JSONAPI
4
4
  module ActsAsResourceController
5
+ MEDIA_TYPE_MATCHER = /(.+".+"[^,]*|[^,]+)/
6
+ ALL_MEDIA_TYPES = '*/*'
5
7
 
6
8
  def self.included(base)
7
9
  base.extend ClassMethods
10
+ base.include Callbacks
8
11
  base.before_action :ensure_correct_media_type, only: [:create, :update, :create_relationship, :update_relationship]
12
+ base.before_action :ensure_valid_accept_media_type
9
13
  base.cattr_reader :server_error_callbacks
14
+ base.define_jsonapi_resources_callbacks :process_operations
10
15
  end
11
16
 
12
17
  def index
@@ -54,27 +59,44 @@ module JSONAPI
54
59
  end
55
60
 
56
61
  def process_request
57
- @request = JSONAPI::Request.new(params, context: context,
58
- key_formatter: key_formatter,
59
- server_error_callbacks: (self.class.server_error_callbacks || []))
62
+ @request = JSONAPI::RequestParser.new(params, context: context,
63
+ key_formatter: key_formatter,
64
+ server_error_callbacks: (self.class.server_error_callbacks || []))
60
65
  unless @request.errors.empty?
61
66
  render_errors(@request.errors)
62
67
  else
63
- operation_results = create_operations_processor.process(@request)
64
- render_results(operation_results)
68
+ process_operations
69
+ render_results(@operation_results)
65
70
  end
66
71
 
67
72
  rescue => e
68
73
  handle_exceptions(e)
69
- ensure
70
- if response.body.size > 0
71
- response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
74
+ end
75
+
76
+ def process_operations
77
+ run_callbacks :process_operations do
78
+ @operation_results = operation_dispatcher.process(@request.operations)
72
79
  end
73
80
  end
74
81
 
75
- # set the operations processor in the configuration or override this to use another operations processor
76
- def create_operations_processor
77
- JSONAPI.configuration.operations_processor.new
82
+ def transaction
83
+ lambda { |&block|
84
+ ActiveRecord::Base.transaction do
85
+ block.yield
86
+ end
87
+ }
88
+ end
89
+
90
+ def rollback
91
+ lambda {
92
+ fail ActiveRecord::Rollback
93
+ }
94
+ end
95
+
96
+ def operation_dispatcher
97
+ @operation_dispatcher ||= JSONAPI::OperationDispatcher.new(transaction: transaction,
98
+ rollback: rollback,
99
+ server_error_callbacks: @request.server_error_callbacks)
78
100
  end
79
101
 
80
102
  private
@@ -103,6 +125,36 @@ module JSONAPI
103
125
  handle_exceptions(e)
104
126
  end
105
127
 
128
+ def ensure_valid_accept_media_type
129
+ if invalid_accept_media_type?
130
+ fail JSONAPI::Exceptions::NotAcceptableError.new(request.accept)
131
+ end
132
+ rescue => e
133
+ handle_exceptions(e)
134
+ end
135
+
136
+ def invalid_accept_media_type?
137
+ media_types = media_types_for('Accept')
138
+
139
+ return false if media_types.blank? || media_types.include?(ALL_MEDIA_TYPES)
140
+
141
+ jsonapi_media_types = media_types.select do |media_type|
142
+ media_type.include?(JSONAPI::MEDIA_TYPE)
143
+ end
144
+
145
+ jsonapi_media_types.size.zero? ||
146
+ jsonapi_media_types.none? do |media_type|
147
+ media_type == JSONAPI::MEDIA_TYPE
148
+ end
149
+ end
150
+
151
+ def media_types_for(header)
152
+ (request.headers[header] || '')
153
+ .match(MEDIA_TYPE_MATCHER)
154
+ .to_a
155
+ .map(&:strip)
156
+ end
157
+
106
158
  # override to set context
107
159
  def context
108
160
  {}
@@ -117,7 +169,7 @@ module JSONAPI
117
169
  # JSONAPI.configuration.route = :camelized_route
118
170
  #
119
171
  # Override if you want to set a per controller key format.
120
- # Must return a class derived from KeyFormatter.
172
+ # Must return an instance of a class derived from KeyFormatter.
121
173
  def key_formatter
122
174
  JSONAPI.configuration.key_formatter
123
175
  end
@@ -152,7 +204,18 @@ module JSONAPI
152
204
 
153
205
  def render_results(operation_results)
154
206
  response_doc = create_response_document(operation_results)
155
- render status: response_doc.status, json: response_doc.contents
207
+
208
+ render_options = {
209
+ status: response_doc.status,
210
+ json: response_doc.contents,
211
+ content_type: JSONAPI::MEDIA_TYPE
212
+ }
213
+
214
+ render_options[:location] = response_doc.contents[:data]["links"][:self] if (
215
+ response_doc.status == :created && response_doc.contents[:data].class != Array
216
+ )
217
+
218
+ render(render_options)
156
219
  end
157
220
 
158
221
  def create_response_document(operation_results)
@@ -179,7 +242,7 @@ module JSONAPI
179
242
  when JSONAPI::Exceptions::Error
180
243
  render_errors(e.errors)
181
244
  else
182
- if JSONAPI.configuration.exception_class_whitelist.any? { |k| e.class.ancestors.include?(k) }
245
+ if JSONAPI.configuration.exception_class_whitelisted?(e)
183
246
  fail e
184
247
  else
185
248
  internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
@@ -1,29 +1,30 @@
1
1
  require 'jsonapi/formatter'
2
- require 'jsonapi/operations_processor'
3
- require 'jsonapi/active_record_operations_processor'
2
+ require 'jsonapi/processor'
3
+ require 'concurrent'
4
4
 
5
5
  module JSONAPI
6
6
  class Configuration
7
7
  attr_reader :json_key_format,
8
8
  :resource_key_type,
9
- :key_formatter,
10
9
  :route_format,
11
- :route_formatter,
12
10
  :raise_if_parameters_not_allowed,
13
- :operations_processor,
14
11
  :allow_include,
15
12
  :allow_sort,
16
13
  :allow_filter,
17
14
  :default_paginator,
18
15
  :default_page_size,
19
16
  :maximum_page_size,
17
+ :default_processor_klass,
20
18
  :use_text_errors,
21
19
  :top_level_links_include_pagination,
22
20
  :top_level_meta_include_record_count,
23
21
  :top_level_meta_record_count_key,
22
+ :top_level_meta_include_page_count,
23
+ :top_level_meta_page_count_key,
24
24
  :exception_class_whitelist,
25
25
  :always_include_to_one_linkage_data,
26
- :always_include_to_many_linkage_data
26
+ :always_include_to_many_linkage_data,
27
+ :cache_formatters
27
28
 
28
29
  def initialize
29
30
  #:underscored_key, :camelized_key, :dasherized_key, or custom
@@ -32,9 +33,6 @@ module JSONAPI
32
33
  #:underscored_route, :camelized_route, :dasherized_route, or custom
33
34
  self.route_format = :dasherized_route
34
35
 
35
- #:basic, :active_record, or custom
36
- self.operations_processor = :active_record
37
-
38
36
  #:integer, :uuid, :string, or custom (provide a proc)
39
37
  self.resource_key_type = :integer
40
38
 
@@ -59,6 +57,9 @@ module JSONAPI
59
57
  self.top_level_meta_include_record_count = false
60
58
  self.top_level_meta_record_count_key = :record_count
61
59
 
60
+ self.top_level_meta_include_page_count = false
61
+ self.top_level_meta_page_count_key = :page_count
62
+
62
63
  self.use_text_errors = false
63
64
 
64
65
  # List of classes that should not be rescued by the operations processor.
@@ -74,25 +75,81 @@ module JSONAPI
74
75
  # NOTE: always_include_to_many_linkage_data is not currently implemented
75
76
  self.always_include_to_one_linkage_data = false
76
77
  self.always_include_to_many_linkage_data = false
78
+
79
+ # The default Operation Processor to use if one is not defined specifically
80
+ # for a Resource.
81
+ self.default_processor_klass = JSONAPI::Processor
82
+
83
+ # Formatter Caching
84
+ # Set to false to disable caching of string operations on keys and links.
85
+ self.cache_formatters = true
86
+ end
87
+
88
+ def cache_formatters=(bool)
89
+ @cache_formatters = bool
90
+ if bool
91
+ @key_formatter_tlv = Concurrent::ThreadLocalVar.new
92
+ @route_formatter_tlv = Concurrent::ThreadLocalVar.new
93
+ else
94
+ @key_formatter_tlv = nil
95
+ @route_formatter_tlv = nil
96
+ end
77
97
  end
78
98
 
79
99
  def json_key_format=(format)
80
100
  @json_key_format = format
81
- @key_formatter = JSONAPI::Formatter.formatter_for(format)
101
+ if @cache_formatters
102
+ @key_formatter_tlv = Concurrent::ThreadLocalVar.new
103
+ end
104
+ end
105
+
106
+ def route_format=(format)
107
+ @route_format = format
108
+ if @cache_formatters
109
+ @route_formatter_tlv = Concurrent::ThreadLocalVar.new
110
+ end
111
+ end
112
+
113
+ def key_formatter
114
+ if self.cache_formatters
115
+ formatter = @key_formatter_tlv.value
116
+ return formatter if formatter
117
+ end
118
+
119
+ formatter = JSONAPI::Formatter.formatter_for(self.json_key_format)
120
+
121
+ if self.cache_formatters
122
+ formatter = @key_formatter_tlv.value = formatter.cached
123
+ end
124
+
125
+ return formatter
82
126
  end
83
127
 
84
128
  def resource_key_type=(key_type)
85
129
  @resource_key_type = key_type
86
130
  end
87
131
 
88
- def route_format=(format)
89
- @route_format = format
90
- @route_formatter = JSONAPI::Formatter.formatter_for(format)
132
+ def route_formatter
133
+ if self.cache_formatters
134
+ formatter = @route_formatter_tlv.value
135
+ return formatter if formatter
136
+ end
137
+
138
+ formatter = JSONAPI::Formatter.formatter_for(self.route_format)
139
+
140
+ if self.cache_formatters
141
+ formatter = @route_formatter_tlv.value = formatter.cached
142
+ end
143
+
144
+ return formatter
145
+ end
146
+
147
+ def exception_class_whitelisted?(e)
148
+ @exception_class_whitelist.flatten.any? { |k| e.class.ancestors.include?(k) }
91
149
  end
92
150
 
93
- def operations_processor=(operations_processor)
94
- @operations_processor_name = operations_processor
95
- @operations_processor = JSONAPI::OperationsProcessor.operations_processor_for(@operations_processor_name)
151
+ def default_processor_klass=(default_processor_klass)
152
+ @default_processor_klass = default_processor_klass
96
153
  end
97
154
 
98
155
  attr_writer :allow_include, :allow_sort, :allow_filter
@@ -111,6 +168,10 @@ module JSONAPI
111
168
 
112
169
  attr_writer :top_level_meta_record_count_key
113
170
 
171
+ attr_writer :top_level_meta_include_page_count
172
+
173
+ attr_writer :top_level_meta_page_count_key
174
+
114
175
  attr_writer :exception_class_whitelist
115
176
 
116
177
  attr_writer :always_include_to_one_linkage_data
@@ -18,6 +18,12 @@ module JSONAPI
18
18
  @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[options[:status]].to_s
19
19
  @meta = options[:meta]
20
20
  end
21
+
22
+ def to_hash
23
+ hash = {}
24
+ instance_variables.each {|var| hash[var.to_s.delete('@')] = instance_variable_get(var) unless instance_variable_get(var).nil? }
25
+ hash
26
+ end
21
27
  end
22
28
 
23
29
  class Warning
@@ -31,5 +37,11 @@ module JSONAPI
31
37
  options[:code]
32
38
  end
33
39
  end
40
+
41
+ def to_hash
42
+ hash = {}
43
+ instance_variables.each {|var| hash[var.to_s.delete('@')] = instance_variable_get(var) unless instance_variable_get(var).nil? }
44
+ hash
45
+ end
34
46
  end
35
47
  end
@@ -22,6 +22,7 @@ module JSONAPI
22
22
  SAVE_FAILED = '121'
23
23
  FORBIDDEN = '403'
24
24
  RECORD_NOT_FOUND = '404'
25
+ NOT_ACCEPTABLE = '406'
25
26
  UNSUPPORTED_MEDIA_TYPE = '415'
26
27
  LOCKED = '423'
27
28
  INTERNAL_SERVER_ERROR = '500'
@@ -50,6 +51,7 @@ module JSONAPI
50
51
  SAVE_FAILED => 'SAVE_FAILED',
51
52
  FORBIDDEN => 'FORBIDDEN',
52
53
  RECORD_NOT_FOUND => 'RECORD_NOT_FOUND',
54
+ NOT_ACCEPTABLE => 'NOT_ACCEPTABLE',
53
55
  UNSUPPORTED_MEDIA_TYPE => 'UNSUPPORTED_MEDIA_TYPE',
54
56
  LOCKED => 'LOCKED',
55
57
  INTERNAL_SERVER_ERROR => 'INTERNAL_SERVER_ERROR'
@@ -18,9 +18,9 @@ module JSONAPI
18
18
 
19
19
  [JSONAPI::Error.new(code: JSONAPI::INTERNAL_SERVER_ERROR,
20
20
  status: :internal_server_error,
21
- title: I18n.t('jsonapi-resources.exceptions.internal_server_error.title',
21
+ title: I18n.t('jsonapi-resources.exceptions.internal_server_error.title',
22
22
  default: 'Internal Server Error'),
23
- detail: I18n.t('jsonapi-resources.exceptions.internal_server_error.detail',
23
+ detail: I18n.t('jsonapi-resources.exceptions.internal_server_error.detail',
24
24
  default: 'Internal Server Error'),
25
25
  meta: meta)]
26
26
  end
@@ -35,9 +35,9 @@ module JSONAPI
35
35
  def errors
36
36
  [JSONAPI::Error.new(code: JSONAPI::INVALID_RESOURCE,
37
37
  status: :bad_request,
38
- title: I18n.t('jsonapi-resources.exceptions.invalid_resource.title',
38
+ title: I18n.t('jsonapi-resources.exceptions.invalid_resource.title',
39
39
  default: 'Invalid resource'),
40
- detail: I18n.t('jsonapi-resources.exceptions.invalid_resource.detail',
40
+ detail: I18n.t('jsonapi-resources.exceptions.invalid_resource.detail',
41
41
  default: "#{resource} is not a valid resource.", resource: resource))]
42
42
  end
43
43
  end
@@ -51,9 +51,9 @@ module JSONAPI
51
51
  def errors
52
52
  [JSONAPI::Error.new(code: JSONAPI::RECORD_NOT_FOUND,
53
53
  status: :not_found,
54
- title: I18n.translate('jsonapi-resources.exceptions.record_not_found.title',
54
+ title: I18n.translate('jsonapi-resources.exceptions.record_not_found.title',
55
55
  default: 'Record not found'),
56
- detail: I18n.translate('jsonapi-resources.exceptions.record_not_found.detail',
56
+ detail: I18n.translate('jsonapi-resources.exceptions.record_not_found.detail',
57
57
  default: "The record identified by #{id} could not be found.", id: id))]
58
58
  end
59
59
  end
@@ -67,7 +67,7 @@ module JSONAPI
67
67
  def errors
68
68
  [JSONAPI::Error.new(code: JSONAPI::UNSUPPORTED_MEDIA_TYPE,
69
69
  status: :unsupported_media_type,
70
- title: I18n.translate('jsonapi-resources.exceptions.unsupported_media_type.title',
70
+ title: I18n.translate('jsonapi-resources.exceptions.unsupported_media_type.title',
71
71
  default: 'Unsupported media type'),
72
72
  detail: I18n.translate('jsonapi-resources.exceptions.unsupported_media_type.detail',
73
73
  default: "All requests that create or update must use the '#{JSONAPI::MEDIA_TYPE}' Content-Type. This request specified '#{media_type}'.",
@@ -76,6 +76,26 @@ module JSONAPI
76
76
  end
77
77
  end
78
78
 
79
+ class NotAcceptableError < Error
80
+ attr_accessor :media_type
81
+
82
+ def initialize(media_type)
83
+ @media_type = media_type
84
+ end
85
+
86
+ def errors
87
+ [JSONAPI::Error.new(code: JSONAPI::NOT_ACCEPTABLE,
88
+ status: :not_acceptable,
89
+ title: I18n.translate('jsonapi-resources.exceptions.not_acceptable.title',
90
+ default: 'Not acceptable'),
91
+ detail: I18n.translate('jsonapi-resources.exceptions.not_acceptable.detail',
92
+ default: "All requests must use the '#{JSONAPI::MEDIA_TYPE}' Accept without media type parameters. This request specified '#{media_type}'.",
93
+ needed_media_type: JSONAPI::MEDIA_TYPE,
94
+ media_type: media_type))]
95
+ end
96
+ end
97
+
98
+
79
99
  class HasManyRelationExists < Error
80
100
  attr_accessor :id
81
101
  def initialize(id)
@@ -85,7 +105,7 @@ module JSONAPI
85
105
  def errors
86
106
  [JSONAPI::Error.new(code: JSONAPI::RELATION_EXISTS,
87
107
  status: :bad_request,
88
- title: I18n.translate('jsonapi-resources.exceptions.has_many_relation.title',
108
+ title: I18n.translate('jsonapi-resources.exceptions.has_many_relation.title',
89
109
  default: 'Relation exists'),
90
110
  detail: I18n.translate('jsonapi-resources.exceptions.has_many_relation.detail',
91
111
  default: "The relation to #{id} already exists.",
@@ -97,7 +117,7 @@ module JSONAPI
97
117
  def errors
98
118
  [JSONAPI::Error.new(code: JSONAPI::FORBIDDEN,
99
119
  status: :forbidden,
100
- title: I18n.translate('jsonapi-resources.exceptions.to_many_set_replacement_forbidden.title',
120
+ title: I18n.translate('jsonapi-resources.exceptions.to_many_set_replacement_forbidden.title',
101
121
  default: 'Complete replacement forbidden'),
102
122
  detail: I18n.translate('jsonapi-resources.exceptions.to_many_set_replacement_forbidden.detail',
103
123
  default: 'Complete replacement forbidden for this relationship'))]