jsonapi.rb 2.0.1 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/lib/jsonapi/active_model_error_serializer.rb +4 -24
- data/lib/jsonapi/deserialization.rb +1 -1
- data/lib/jsonapi/error_serializer.rb +1 -1
- data/lib/jsonapi/fetching.rb +2 -2
- data/lib/jsonapi/filtering.rb +1 -2
- data/lib/jsonapi/pagination.rb +4 -11
- data/lib/jsonapi/rails.rb +4 -24
- data/lib/jsonapi/version.rb +1 -1
- data/spec/dummy.rb +23 -0
- data/spec/errors_spec.rb +93 -67
- data/spec/pagination_spec.rb +24 -3
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ab8c32a2d9e284664f914ce8ab96260bb0811378cf7bb9d1b061ee4f9b642c9
|
4
|
+
data.tar.gz: 166da57b4b87d9ede4a9088da6aa7e3d277db3be9ef21529784367f4621536ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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,
|
23
|
-
|
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
|
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.
|
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
|
data/lib/jsonapi/fetching.rb
CHANGED
@@ -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(',').
|
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(',').
|
32
|
+
params['include'].to_s.split(',').filter_map(&:strip)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
data/lib/jsonapi/filtering.rb
CHANGED
data/lib/jsonapi/pagination.rb
CHANGED
@@ -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
|
-
|
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: {
|
59
|
+
resource.errors, params: {
|
60
|
+
model: model,
|
61
|
+
model_serializer: model_serializer
|
62
|
+
}
|
83
63
|
)
|
84
64
|
)
|
85
65
|
end
|
data/lib/jsonapi/version.rb
CHANGED
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']
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
54
|
-
|
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
|
-
|
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']
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
expect(response_json['errors']
|
115
|
-
|
116
|
-
|
117
|
-
|
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']
|
133
|
-
|
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']
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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']
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
data/spec/pagination_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
}.
|
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.
|
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:
|
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: '
|
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: '
|
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.
|
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...
|