jsonapi.rb 2.0.1 → 2.1.1

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
  SHA256:
3
- metadata.gz: d531366279855044f83d35f4526a72156c711895290ab4dbce73f1c7615f1079
4
- data.tar.gz: 63bd2be61a78d79a64bc739cbb492ee75a9e2f90a95389d55f133a2565627abb
3
+ metadata.gz: 1ab8c32a2d9e284664f914ce8ab96260bb0811378cf7bb9d1b061ee4f9b642c9
4
+ data.tar.gz: 166da57b4b87d9ede4a9088da6aa7e3d277db3be9ef21529784367f4621536ca
5
5
  SHA512:
6
- metadata.gz: e602d6c993cd0259ac0b81c57eca16aee525f53ed0e5b4ac8014a0cb500528d579541375ce7735f0f191f3f732af9178ebbfdf7d4615eb967cfb244d1796cd36
7
- data.tar.gz: 46a957ded4247be1607f6a4d84246da42a44fe41c58c55e2a34a914e958a28731531bc5e8a3f0d0141f571e17dce19c9c1f6f58845fc3e7db874d763dfcaffbb
6
+ metadata.gz: 0d274a7ae6f8931c1009ba02af6d595efc02fb3c832225327b6ec96280d863a51ed6e36283621e53a76a568565a45e1762676a10d336eecdc080a1690ee97572
7
+ data.tar.gz: a04bf92af8f7484d54c6ac1c01703f0673197732f3b52a7de148f38b2d99ad9f936935192942c9607a13ffd1350254ca3a976b064a026bdd0dd8d269e4b81ff0
data/README.md CHANGED
@@ -233,6 +233,12 @@ class MyController < ActionController::Base
233
233
  end
234
234
  ```
235
235
 
236
+ This allows you to run queries like:
237
+
238
+ ```bash
239
+ $ curl -X GET /api/resources?fields[model]=model_attr,relationship
240
+ ```
241
+
236
242
  ### Filtering and sorting
237
243
 
238
244
  `JSONAPI::Filtering` uses the power of
@@ -276,7 +282,7 @@ grouping. To enable expressions along with filters, use the option flags:
276
282
  ```ruby
277
283
  options = { sort_with_expressions: true }
278
284
  jsonapi_filter(User.all, allowed_fields, options) do |filtered|
279
- render jsonapi: result.group('id').to_a
285
+ render jsonapi: filtered.result.group('id').to_a
280
286
  end
281
287
  ```
282
288
 
@@ -12,35 +12,15 @@ module JSONAPI
12
12
  end
13
13
 
14
14
  attribute :code do |object|
15
- _, error_hash = object
16
- code = error_hash[:error] unless error_hash[:error].is_a?(Hash)
17
- code ||= error_hash[:message] || :invalid
18
- # `parameterize` separator arguments are different on Rails 4 vs 5...
19
- code.to_s.delete("''").parameterize.tr('-', '_')
15
+ object.type.to_s.delete("''").parameterize.tr('-', '_')
20
16
  end
21
17
 
22
- attribute :detail do |object, params|
23
- error_key, error_hash = object
24
- errors_object = params[:model].errors
25
-
26
- # Rails 4 provides just the message.
27
- if error_hash[:error].present? && error_hash[:error].is_a?(Hash)
28
- message = errors_object.generate_message(
29
- error_key, nil, error_hash[:error]
30
- )
31
- elsif error_hash[:error].present?
32
- message = errors_object.generate_message(
33
- error_key, error_hash[:error], error_hash
34
- )
35
- else
36
- message = error_hash[:message]
37
- end
38
-
39
- errors_object.full_message(error_key, message)
18
+ attribute :detail do |object, _params|
19
+ object.full_message
40
20
  end
41
21
 
42
22
  attribute :source do |object, params|
43
- error_key, _ = object
23
+ error_key = object.attribute
44
24
  model_serializer = params[:model_serializer]
45
25
  attrs = (model_serializer.attributes_to_serialize || {}).keys
46
26
  rels = (model_serializer.relationships_to_serialize || {}).keys
@@ -65,7 +65,7 @@ module JSONAPI
65
65
  rel_name = jsonapi_inflector.singularize(assoc_name)
66
66
 
67
67
  if assoc_data.is_a?(Array)
68
- parsed["#{rel_name}_ids"] = assoc_data.map { |ri| ri['id'] }.compact
68
+ parsed["#{rel_name}_ids"] = assoc_data.filter_map { |ri| ri['id'] }
69
69
  next
70
70
  end
71
71
 
@@ -8,7 +8,7 @@ module JSONAPI
8
8
  set_type :error
9
9
 
10
10
  # Object/Hash attribute helpers.
11
- [:status, :source, :title, :detail].each do |attr_name|
11
+ [:status, :source, :title, :detail, :code].each do |attr_name|
12
12
  attribute attr_name do |object|
13
13
  object.try(attr_name) || object.try(:fetch, attr_name, nil)
14
14
  end
@@ -17,7 +17,7 @@ module JSONAPI
17
17
  end
18
18
 
19
19
  params[:fields].each do |k, v|
20
- extracted[k] = v.to_s.split(',').map(&:strip).compact
20
+ extracted[k] = v.to_s.split(',').filter_map(&:strip)
21
21
  end
22
22
 
23
23
  extracted
@@ -29,7 +29,7 @@ module JSONAPI
29
29
  #
30
30
  # @return [Array]
31
31
  def jsonapi_include
32
- params['include'].to_s.split(',').map(&:strip).compact
32
+ params['include'].to_s.split(',').filter_map(&:strip)
33
33
  end
34
34
  end
35
35
  end
@@ -1,6 +1,5 @@
1
1
  begin
2
- require 'active_record'
3
- require 'ransack'
2
+ require 'ransack/predicate'
4
3
  require_relative 'patches'
5
4
  rescue LoadError
6
5
  end
@@ -13,14 +13,13 @@ module JSONAPI
13
13
  def jsonapi_paginate(resources)
14
14
  offset, limit, _ = jsonapi_pagination_params
15
15
 
16
+ # Cache the original resources size to be used for pagination meta
17
+ @_jsonapi_original_size = resources.size
18
+
16
19
  if resources.respond_to?(:offset)
17
20
  resources = resources.offset(offset).limit(limit)
18
21
  else
19
- original_size = resources.size
20
22
  resources = resources[(offset)..(offset + limit - 1)] || []
21
-
22
- # Cache the original resources size to be used for pagination meta
23
- resources.instance_variable_set(:@original_size, original_size)
24
23
  end
25
24
 
26
25
  block_given? ? yield(resources) : resources
@@ -64,13 +63,7 @@ module JSONAPI
64
63
 
65
64
  numbers = { current: page }
66
65
 
67
- if resources.respond_to?(:unscope)
68
- total = resources.unscope(:limit, :offset, :order).size
69
- else
70
- # Try to fetch the cached size first
71
- total = resources.instance_variable_get(:@original_size)
72
- total ||= resources.size
73
- end
66
+ total = @_jsonapi_original_size
74
67
 
75
68
  last_page = [1, (total.to_f / limit).ceil].max
76
69
 
data/lib/jsonapi/rails.rb CHANGED
@@ -46,7 +46,6 @@ module JSONAPI
46
46
  JSONAPI::ErrorSerializer.new(resource, options)
47
47
  ) unless resource.is_a?(ActiveModel::Errors)
48
48
 
49
- errors = []
50
49
  model = resource.instance_variable_get(:@base)
51
50
 
52
51
  if respond_to?(:jsonapi_serializer_class, true)
@@ -55,31 +54,12 @@ module JSONAPI
55
54
  model_serializer = JSONAPI::Rails.serializer_class(model, false)
56
55
  end
57
56
 
58
- details = {}
59
- if ::Rails.gem_version >= Gem::Version.new('6.1')
60
- resource.each do |error|
61
- attr = error.attribute
62
- details[attr] ||= []
63
- details[attr] << error.detail.merge(message: error.message)
64
- end
65
- elsif resource.respond_to?(:details)
66
- details = resource.details
67
- else
68
- details = resource.messages
69
- end
70
-
71
- details.each do |error_key, error_hashes|
72
- error_hashes.each do |error_hash|
73
- # Rails 4 provides just the message.
74
- error_hash = { message: error_hash } unless error_hash.is_a?(Hash)
75
-
76
- errors << [ error_key, error_hash ]
77
- end
78
- end
79
-
80
57
  JSONAPI::Rails.serializer_to_json(
81
58
  JSONAPI::ActiveModelErrorSerializer.new(
82
- errors, params: { model: model, model_serializer: model_serializer }
59
+ resource.errors, params: {
60
+ model: model,
61
+ model_serializer: model_serializer
62
+ }
83
63
  )
84
64
  )
85
65
  end
@@ -1,3 +1,3 @@
1
1
  module JSONAPI
2
- VERSION = '2.0.1'
2
+ VERSION = '2.1.1'
3
3
  end
data/spec/dummy.rb CHANGED
@@ -30,12 +30,34 @@ end
30
30
 
31
31
  class User < ActiveRecord::Base
32
32
  has_many :notes
33
+
34
+ def self.ransackable_attributes(auth_object = nil)
35
+ %w(created_at first_name id last_name updated_at)
36
+ end
37
+
38
+ def self.ransackable_associations(auth_object = nil)
39
+ %w(notes)
40
+ end
33
41
  end
34
42
 
35
43
  class Note < ActiveRecord::Base
44
+ validate :title_cannot_contain_slurs
36
45
  validates_format_of :title, without: /BAD_TITLE/
37
46
  validates_numericality_of :quantity, less_than: 100, if: :quantity?
38
47
  belongs_to :user, required: true
48
+
49
+ def self.ransackable_associations(auth_object = nil)
50
+ %w(user)
51
+ end
52
+
53
+ def self.ransackable_attributes(auth_object = nil)
54
+ %w(created_at id quantity title updated_at user_id)
55
+ end
56
+
57
+ private
58
+ def title_cannot_contain_slurs
59
+ errors.add(:base, 'Title has slurs') if title.to_s.include?('SLURS')
60
+ end
39
61
  end
40
62
 
41
63
  class CustomNoteSerializer
@@ -97,6 +119,7 @@ class UsersController < ActionController::Base
97
119
  result = result.to_a if params[:as_list]
98
120
 
99
121
  jsonapi_paginate(result) do |paginated|
122
+ paginated = paginated.to_a if params[:decorate_after_pagination]
100
123
  render jsonapi: paginated
101
124
  end
102
125
  end
data/spec/errors_spec.rb CHANGED
@@ -32,11 +32,15 @@ RSpec.describe NotesController, type: :request do
32
32
  it do
33
33
  expect(response).to have_http_status(:unprocessable_entity)
34
34
  expect(response_json['errors'].size).to eq(1)
35
- expect(response_json['errors'][0]['status']).to eq('422')
36
- expect(response_json['errors'][0]['title'])
37
- .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
38
- expect(response_json['errors'][0]['source']).to eq('pointer' => '')
39
- expect(response_json['errors'][0]['detail']).to be_nil
35
+ expect(response_json['errors']).to contain_exactly(
36
+ {
37
+ 'status' => '422',
38
+ 'source' => { 'pointer' => '' },
39
+ 'title' => 'Unprocessable Entity',
40
+ 'detail' => nil,
41
+ 'code' => nil
42
+ }
43
+ )
40
44
  end
41
45
  end
42
46
 
@@ -50,19 +54,20 @@ RSpec.describe NotesController, type: :request do
50
54
  it do
51
55
  expect(response).to have_http_status(:unprocessable_entity)
52
56
  expect(response_json['errors'].size).to eq(1)
53
- expect(response_json['errors'][0]['status']).to eq('422')
54
- expect(response_json['errors'][0]['code']).to include('blank')
55
- expect(response_json['errors'][0]['title'])
56
- .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
57
- expect(response_json['errors'][0]['source'])
58
- .to eq('pointer' => '/data/relationships/user')
59
- if Rails.gem_version >= Gem::Version.new('6.1')
60
- expect(response_json['errors'][0]['detail'])
61
- .to eq('User must exist')
57
+ expected_detail = if Rails.gem_version >= Gem::Version.new('6.1')
58
+ 'User must exist'
62
59
  else
63
- expect(response_json['errors'][0]['detail'])
64
- .to eq('User can\'t be blank')
60
+ 'User can\'t be blank'
65
61
  end
62
+ expect(response_json['errors']).to contain_exactly(
63
+ {
64
+ 'status' => '422',
65
+ 'source' => { 'pointer' => '/data/relationships/user' },
66
+ 'title' => 'Unprocessable Entity',
67
+ 'detail' => expected_detail,
68
+ 'code' => 'blank'
69
+ }
70
+ )
66
71
  end
67
72
 
68
73
  context 'required by validations' do
@@ -76,45 +81,51 @@ RSpec.describe NotesController, type: :request do
76
81
  it do
77
82
  expect(response).to have_http_status(:unprocessable_entity)
78
83
  expect(response_json['errors'].size).to eq(3)
79
- expect(response_json['errors'][0]['status']).to eq('422')
80
- expect(response_json['errors'][0]['code']).to include('invalid')
81
- expect(response_json['errors'][0]['title'])
82
- .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
83
- expect(response_json['errors'][0]['source'])
84
- .to eq('pointer' => '/data/attributes/title')
85
- expect(response_json['errors'][0]['detail'])
86
- .to eq('Title is invalid')
87
-
88
- expect(response_json['errors'][1]['status']).to eq('422')
89
-
90
- if Rails::VERSION::MAJOR >= 5
91
- expect(response_json['errors'][1]['code']).to eq('invalid')
92
- else
93
- expect(response_json['errors'][1]['code']).to eq('has_typos')
94
- end
95
-
96
- expect(response_json['errors'][1]['title'])
97
- .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
98
- expect(response_json['errors'][1]['source'])
99
- .to eq('pointer' => '/data/attributes/title')
100
- expect(response_json['errors'][1]['detail'])
101
- .to eq('Title has typos')
102
-
103
- expect(response_json['errors'][2]['status']).to eq('422')
104
-
105
- if Rails::VERSION::MAJOR >= 5
106
- expect(response_json['errors'][2]['code']).to eq('less_than')
107
- else
108
- expect(response_json['errors'][2]['code'])
109
- .to eq('must_be_less_than_100')
110
- end
111
-
112
- expect(response_json['errors'][2]['title'])
113
- .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
114
- expect(response_json['errors'][2]['source'])
115
- .to eq('pointer' => '/data/attributes/quantity')
116
- expect(response_json['errors'][2]['detail'])
117
- .to eq('Quantity must be less than 100')
84
+ expect(response_json['errors']).to contain_exactly(
85
+ {
86
+ 'status' => '422',
87
+ 'source' => { 'pointer' => '/data/attributes/title' },
88
+ 'title' => 'Unprocessable Entity',
89
+ 'detail' => 'Title is invalid',
90
+ 'code' => 'invalid'
91
+ },
92
+ {
93
+ 'status' => '422',
94
+ 'source' => { 'pointer' => '/data/attributes/title' },
95
+ 'title' => 'Unprocessable Entity',
96
+ 'detail' => 'Title has typos',
97
+ 'code' => 'invalid'
98
+ },
99
+ {
100
+ 'status' => '422',
101
+ 'source' => { 'pointer' => '/data/attributes/quantity' },
102
+ 'title' => 'Unprocessable Entity',
103
+ 'detail' => 'Quantity must be less than 100',
104
+ 'code' => 'less_than'
105
+ }
106
+ )
107
+ end
108
+ end
109
+
110
+ context 'validations with non-interpolated messages' do
111
+ let(:params) do
112
+ payload = note_params.dup
113
+ payload[:data][:attributes][:title] = 'SLURS ARE GREAT'
114
+ payload
115
+ end
116
+
117
+ it do
118
+ expect(response).to have_http_status(:unprocessable_entity)
119
+ expect(response_json['errors'].size).to eq(1)
120
+ expect(response_json['errors']).to contain_exactly(
121
+ {
122
+ 'status' => '422',
123
+ 'source' => { 'pointer' => '' },
124
+ 'title' => 'Unprocessable Entity',
125
+ 'detail' => 'Title has slurs',
126
+ 'code' => 'title_has_slurs'
127
+ }
128
+ )
118
129
  end
119
130
  end
120
131
 
@@ -129,8 +140,15 @@ RSpec.describe NotesController, type: :request do
129
140
 
130
141
  it do
131
142
  expect(response).to have_http_status(:unprocessable_entity)
132
- expect(response_json['errors'][0]['source'])
133
- .to eq('pointer' => '/data/attributes/title')
143
+ expect(response_json['errors']).to contain_exactly(
144
+ {
145
+ 'status' => '422',
146
+ 'source' => { 'pointer' => '/data/attributes/title' },
147
+ 'title' => 'Unprocessable Entity',
148
+ 'detail' => nil,
149
+ 'code' => nil
150
+ }
151
+ )
134
152
  end
135
153
  end
136
154
  end
@@ -142,11 +160,15 @@ RSpec.describe NotesController, type: :request do
142
160
  it do
143
161
  expect(response).to have_http_status(:not_found)
144
162
  expect(response_json['errors'].size).to eq(1)
145
- expect(response_json['errors'][0]['status']).to eq('404')
146
- expect(response_json['errors'][0]['title'])
147
- .to eq(Rack::Utils::HTTP_STATUS_CODES[404])
148
- expect(response_json['errors'][0]['source']).to be_nil
149
- expect(response_json['errors'][0]['detail']).to be_nil
163
+ expect(response_json['errors']).to contain_exactly(
164
+ {
165
+ 'status' => '404',
166
+ 'source' => nil,
167
+ 'title' => 'Not Found',
168
+ 'detail' => nil,
169
+ 'code' => nil
170
+ }
171
+ )
150
172
  end
151
173
  end
152
174
 
@@ -157,11 +179,15 @@ RSpec.describe NotesController, type: :request do
157
179
  it do
158
180
  expect(response).to have_http_status(:internal_server_error)
159
181
  expect(response_json['errors'].size).to eq(1)
160
- expect(response_json['errors'][0]['status']).to eq('500')
161
- expect(response_json['errors'][0]['title'])
162
- .to eq(Rack::Utils::HTTP_STATUS_CODES[500])
163
- expect(response_json['errors'][0]['source']).to be_nil
164
- expect(response_json['errors'][0]['detail']).to be_nil
182
+ expect(response_json['errors']).to contain_exactly(
183
+ {
184
+ 'status' => '500',
185
+ 'source' => nil,
186
+ 'title' => 'Internal Server Error',
187
+ 'detail' => nil,
188
+ 'code' => nil
189
+ }
190
+ )
165
191
  end
166
192
  end
167
193
  end
@@ -53,12 +53,14 @@ RSpec.describe UsersController, type: :request do
53
53
 
54
54
  context 'on page 2 out of 3' do
55
55
  let(:as_list) { }
56
+ let(:decorate_after_pagination) { }
56
57
  let(:params) do
57
58
  {
58
59
  page: { number: 2, size: 1 },
59
60
  sort: '-created_at',
60
- as_list: as_list
61
- }.reject { |_k, _v| _v.blank? }
61
+ as_list: as_list,
62
+ decorate_after_pagination: decorate_after_pagination
63
+ }.compact_blank
62
64
  end
63
65
 
64
66
  context 'on an array of resources' do
@@ -80,6 +82,25 @@ RSpec.describe UsersController, type: :request do
80
82
  end
81
83
  end
82
84
 
85
+ context 'when decorating objects after pagination' do
86
+ let(:decorate_after_pagination) { true }
87
+
88
+ it do
89
+ expect(response).to have_http_status(:ok)
90
+ expect(response_json['data'].size).to eq(1)
91
+ expect(response_json['data'][0]).to have_id(second_user.id.to_s)
92
+
93
+ expect(response_json['meta']['pagination']).to eq(
94
+ 'current' => 2,
95
+ 'first' => 1,
96
+ 'prev' => 1,
97
+ 'next' => 3,
98
+ 'last' => 3,
99
+ 'records' => 3
100
+ )
101
+ end
102
+ end
103
+
83
104
  it do
84
105
  expect(response).to have_http_status(:ok)
85
106
  expect(response_json['data'].size).to eq(1)
@@ -157,7 +178,7 @@ RSpec.describe UsersController, type: :request do
157
178
  {
158
179
  page: { number: 5, size: 1 },
159
180
  as_list: as_list
160
- }.reject { |_k, _v| _v.blank? }
181
+ }.compact_blank
161
182
  end
162
183
 
163
184
  context 'on an array of resources' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stas Suscov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-05 00:00:00.000000000 Z
11
+ date: 2024-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializer
@@ -98,16 +98,16 @@ dependencies:
98
98
  name: sqlite3
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '1.7'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '1.7'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: ffaker
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -280,7 +280,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
280
280
  - !ruby/object:Gem::Version
281
281
  version: '0'
282
282
  requirements: []
283
- rubygems_version: 3.3.3
283
+ rubygems_version: 3.4.10
284
284
  signing_key:
285
285
  specification_version: 4
286
286
  summary: So you say you need JSON:API support in your API...