jsonapi.rb 2.0.0 → 2.1.1

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
  SHA256:
3
- metadata.gz: 2192bbffc57d4ecbe419a90e25728951db4464c5f299e8866afefe72fa393cad
4
- data.tar.gz: f7f83b43169207e7cf6d06e6e99cc8c8f705de09c4c83ff9dba5238540577a9e
3
+ metadata.gz: 1ab8c32a2d9e284664f914ce8ab96260bb0811378cf7bb9d1b061ee4f9b642c9
4
+ data.tar.gz: 166da57b4b87d9ede4a9088da6aa7e3d277db3be9ef21529784367f4621536ca
5
5
  SHA512:
6
- metadata.gz: 577c2b0d1801ef2191c25925e407449ff12ba95d7fc5c8433fcbb2b20e532a1b7a51a884f147c5042c38ec27ca9eed6567bfd746aad4ad8895b7a3f90e9043b1
7
- data.tar.gz: f6ca1bc49fd89c1ea942f3c047234d76c4fa1d7604bd06aa5d67c43dad93603cb19e6c542ec13837d1db5e3fbe923ec4799fd865c96c08822dcab7cb160d7f7e
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,9 +1,7 @@
1
1
  begin
2
- require 'active_record'
3
- require 'ransack'
2
+ require 'ransack/predicate'
4
3
  require_relative 'patches'
5
4
  rescue LoadError
6
- warn('Install `ransack` gem before using `JSONAPI::Filtering`!')
7
5
  end
8
6
 
9
7
  # Filtering and sorting support
@@ -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
@@ -37,7 +37,7 @@ module JSONAPI
37
37
  # @return [NilClass]
38
38
  def self.add_errors_renderer!
39
39
  ActionController::Renderers.add(:jsonapi_errors) do |resource, options|
40
- self.content_type ||= Mime[:jsonapi]
40
+ self.content_type = Mime[:jsonapi] if self.media_type.nil?
41
41
 
42
42
  many = JSONAPI::Rails.is_collection?(resource, options[:is_collection])
43
43
  resource = [resource] unless many
@@ -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
@@ -90,7 +70,7 @@ module JSONAPI
90
70
  # @return [NilClass]
91
71
  def self.add_renderer!
92
72
  ActionController::Renderers.add(:jsonapi) do |resource, options|
93
- self.content_type ||= Mime[:jsonapi]
73
+ self.content_type = Mime[:jsonapi] if self.media_type.nil?
94
74
 
95
75
  JSONAPI_METHODS_MAPPING.to_a[0..1].each do |opt, method_name|
96
76
  next unless respond_to?(method_name, true)
@@ -100,7 +80,7 @@ module JSONAPI
100
80
  # If it's an empty collection, return it directly.
101
81
  many = JSONAPI::Rails.is_collection?(resource, options[:is_collection])
102
82
  if many && !resource.any?
103
- return options.slice(:meta, :links).merge(data: []).to_json
83
+ return options.slice(:meta, :links).compact.merge(data: []).to_json
104
84
  end
105
85
 
106
86
  JSONAPI_METHODS_MAPPING.to_a[2..-1].each do |opt, method_name|
@@ -1,3 +1,3 @@
1
1
  module JSONAPI
2
- VERSION = '2.0.0'
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.0
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-03-21 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
@@ -265,7 +265,7 @@ homepage: https://github.com/stas/jsonapi.rb
265
265
  licenses:
266
266
  - MIT
267
267
  metadata: {}
268
- post_install_message:
268
+ post_install_message: Install manually `ransack` gem before using `JSONAPI::Filtering`!
269
269
  rdoc_options: []
270
270
  require_paths:
271
271
  - lib
@@ -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...