jsonapi.rb 1.6.0 → 1.7.0

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: dc1e0239230a7a14a008b6344be8857fccd4db574af564a9b49342842fcc316d
4
- data.tar.gz: 16615a0a1e0bd8b6f0a84ae9f2adcc16005a16b6413577b3ee945fddb6ce90c5
3
+ metadata.gz: 07512bb6e4135e51e0f94a3399ec068e7aa53a13d6eb65b44f5c87be8e4310e4
4
+ data.tar.gz: 128306af20f37d9fb7563a9cbc30f88f68dc3f761783a02eb9db539886c67bc5
5
5
  SHA512:
6
- metadata.gz: 2c65ee96100a9179205b0f8e220f4f42dc5cd0f03fb8c7c645cb1c70933f848e2e0fe11d600259f2001fe1a9d8760b821c4748009ee3451156e2b07a660df783
7
- data.tar.gz: 26c49d61c04f3bc03eaee260137905886a421c31d6bb669c063eb4cca9bea15a77327a07e90eb63ede9bb4b0d5e4b153aa38eb6dd6167c2eeabf10a1d7fb8fd6
6
+ metadata.gz: 6658f11bc2501360cb8be91a05d14d427102905476cd1e0d2a020b1d9db36ce77857127090dc37f27dfb9292c187f3ad8028ea47ab8b26f06ffe7648276009fe
7
+ data.tar.gz: 2cfb89517aba924c3282d3f8f65a1281606407c0e11b97159ebab7b58c75dbf02fbf65962793a031b07e87e9e2cd90a31b87f6e5d0d54a91c769c0eed82e7088
data/README.md CHANGED
@@ -290,6 +290,7 @@ class MyController < ActionController::Base
290
290
  render jsonapi: paginated
291
291
  end
292
292
  end
293
+
293
294
  end
294
295
  ```
295
296
 
@@ -306,6 +307,17 @@ use the `jsonapi_pagination_meta` method:
306
307
  end
307
308
 
308
309
  ```
310
+
311
+ If you want to change the default number of items per page or define a custom logic to handle page size, use the
312
+ `jsonapi_page_size` method:
313
+
314
+ ```ruby
315
+ def jsonapi_page_size(pagination_params)
316
+ per_page = pagination_params[:size].to_f.to_i
317
+ per_page = 30 if per_page > 30
318
+ per_page
319
+ end
320
+ ```
309
321
  ### Deserialization
310
322
 
311
323
  `JSONAPI::Deserialization` provides a helper to transform a `JSONAPI` document
@@ -3,9 +3,6 @@ require 'jsonapi/error_serializer'
3
3
  module JSONAPI
4
4
  # [ActiveModel::Errors] serializer
5
5
  class ActiveModelErrorSerializer < ErrorSerializer
6
- set_id :object_id
7
- set_type :error
8
-
9
6
  attribute :status do
10
7
  '422'
11
8
  end
@@ -5,7 +5,6 @@ module JSONAPI
5
5
  class ErrorSerializer
6
6
  include JSONAPI::Serializer
7
7
 
8
- set_id :object_id
9
8
  set_type :error
10
9
 
11
10
  # Object/Hash attribute helpers.
@@ -15,6 +14,12 @@ module JSONAPI
15
14
  end
16
15
  end
17
16
 
17
+ # Overwrite the ID extraction method, to skip validations
18
+ #
19
+ # @return [NilClass]
20
+ def self.id_from_record(_record, _params)
21
+ end
22
+
18
23
  # Remap the root key to `errors`
19
24
  #
20
25
  # @return [Hash]
@@ -43,6 +43,8 @@ module JSONAPI
43
43
  original_url = request.base_url + request.path + '?'
44
44
 
45
45
  pagination.each do |page_name, number|
46
+ next if page_name == :records
47
+
46
48
  original_params[:page][:number] = number
47
49
  links[page_name] = original_url + CGI.unescape(
48
50
  original_params.to_query
@@ -63,7 +65,7 @@ module JSONAPI
63
65
  numbers = { current: page }
64
66
 
65
67
  if resources.respond_to?(:unscope)
66
- total = resources.unscope(:limit, :offset, :order).count()
68
+ total = resources.unscope(:limit, :offset, :order).size
67
69
  else
68
70
  # Try to fetch the cached size first
69
71
  total = resources.instance_variable_get(:@original_size)
@@ -82,6 +84,10 @@ module JSONAPI
82
84
  numbers[:last] = last_page
83
85
  end
84
86
 
87
+ if total.present?
88
+ numbers[:records] = total
89
+ end
90
+
85
91
  numbers
86
92
  end
87
93
 
@@ -89,16 +95,30 @@ module JSONAPI
89
95
  #
90
96
  # @return [Array] with the offset, limit and the current page number
91
97
  def jsonapi_pagination_params
92
- def_per_page = self.class.const_get(:JSONAPI_PAGE_SIZE).to_i
93
-
94
98
  pagination = params[:page].try(:slice, :number, :size) || {}
95
- per_page = pagination[:size].to_f.to_i
96
- per_page = def_per_page if per_page > def_per_page || per_page < 1
99
+ per_page = jsonapi_page_size(pagination)
97
100
  num = [1, pagination[:number].to_f.to_i].max
98
101
 
99
102
  [(num - 1) * per_page, per_page, num]
100
103
  end
101
104
 
105
+ # Retrieves the default page size
106
+ #
107
+ # @param per_page_param [Hash] opts the paginations params
108
+ # @option opts [String] :number the page number requested
109
+ # @option opts [String] :size the page size requested
110
+ #
111
+ # @return [Integer]
112
+ def jsonapi_page_size(pagination_params)
113
+ per_page = pagination_params[:size].to_f.to_i
114
+
115
+ return self.class
116
+ .const_get(:JSONAPI_PAGE_SIZE)
117
+ .to_i if per_page < 1
118
+
119
+ per_page
120
+ end
121
+
102
122
  # Fallback to Rack's parsed query string when Rails is not available
103
123
  #
104
124
  # @return [Hash]
data/lib/jsonapi/rails.rb CHANGED
@@ -55,8 +55,18 @@ module JSONAPI
55
55
  model_serializer = JSONAPI::Rails.serializer_class(model, false)
56
56
  end
57
57
 
58
- details = resource.messages
59
- details = resource.details if resource.respond_to?(:details)
58
+ details = {}
59
+ if ::Rails::VERSION::MAJOR >= 6 && ::Rails::VERSION::MINOR >= 1
60
+ resource.map 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
60
70
 
61
71
  details.each do |error_key, error_hashes|
62
72
  error_hashes.each do |error_hash|
@@ -1,3 +1,3 @@
1
1
  module JSONAPI
2
- VERSION = '1.6.0'
2
+ VERSION = '1.7.0'
3
3
  end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe JSONAPI::Deserialization do
4
+ let(:jsonapi_deserialize) { UsersController.new.method(:jsonapi_deserialize) }
5
+ let(:document) do
6
+ {
7
+ data: {
8
+ id: 1,
9
+ type: 'note',
10
+ attributes: {
11
+ title: 'Title 1',
12
+ date: '2015-12-20'
13
+ },
14
+ relationships: {
15
+ author: {
16
+ data: {
17
+ type: 'user',
18
+ id: 2
19
+ }
20
+ },
21
+ second_author: {
22
+ data: nil
23
+ },
24
+ notes: {
25
+ data: [
26
+ {
27
+ type: 'note',
28
+ id: 3
29
+ },
30
+ {
31
+ type: 'note',
32
+ id: 4
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ }
38
+ }
39
+ end
40
+
41
+ describe '#jsonapi_deserialize' do
42
+ it do
43
+ expect(jsonapi_deserialize.call(document)).to eq(
44
+ 'id' => 1,
45
+ 'date' => '2015-12-20',
46
+ 'title' => 'Title 1',
47
+ 'author_id' => 2,
48
+ 'second_author_id' => nil,
49
+ 'note_ids' => [3, 4]
50
+ )
51
+ end
52
+
53
+ context 'with `only`' do
54
+ it do
55
+ expect(jsonapi_deserialize.call(document, only: :notes)).to eq(
56
+ 'note_ids' => [3, 4]
57
+ )
58
+ end
59
+ end
60
+
61
+ context 'with `except`' do
62
+ it do
63
+ expect(
64
+ jsonapi_deserialize.call(document, except: [:date, :title])
65
+ ).to eq(
66
+ 'id' => 1,
67
+ 'author_id' => 2,
68
+ 'second_author_id' => nil,
69
+ 'note_ids' => [3, 4]
70
+ )
71
+ end
72
+ end
73
+
74
+ context 'with `polymorphic`' do
75
+ it do
76
+ expect(
77
+ jsonapi_deserialize.call(
78
+ document, only: :author, polymorphic: :author
79
+ )
80
+ ).to eq(
81
+ 'author_id' => 2,
82
+ 'author_type' => User.name
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
data/spec/dummy.rb ADDED
@@ -0,0 +1,163 @@
1
+ require 'securerandom'
2
+ require 'active_record'
3
+ require 'action_controller/railtie'
4
+ require 'jsonapi'
5
+ require 'ransack'
6
+
7
+ Rails.logger = Logger.new(STDOUT)
8
+ Rails.logger.level = ENV['LOG_LEVEL'] || Logger::WARN
9
+
10
+ JSONAPI::Rails.install!
11
+
12
+ ActiveRecord::Base.logger = Rails.logger
13
+ ActiveRecord::Base.establish_connection(
14
+ ENV['DATABASE_URL'] || 'sqlite3::memory:'
15
+ )
16
+
17
+ ActiveRecord::Schema.define do
18
+ create_table :users, force: true do |t|
19
+ t.string :first_name
20
+ t.string :last_name
21
+ t.timestamps
22
+ end
23
+
24
+ create_table :notes, force: true do |t|
25
+ t.string :title
26
+ t.integer :user_id
27
+ t.integer :quantity
28
+ t.timestamps
29
+ end
30
+ end
31
+
32
+ class User < ActiveRecord::Base
33
+ has_many :notes
34
+ end
35
+
36
+ class Note < ActiveRecord::Base
37
+ validates_format_of :title, without: /BAD_TITLE/
38
+ validates_numericality_of :quantity, less_than: 100, if: :quantity?
39
+ belongs_to :user, required: true
40
+ end
41
+
42
+ class CustomNoteSerializer
43
+ include JSONAPI::Serializer
44
+
45
+ set_type :note
46
+ belongs_to :user
47
+ attributes(:title, :quantity, :created_at, :updated_at)
48
+ end
49
+
50
+ class UserSerializer
51
+ include JSONAPI::Serializer
52
+
53
+ has_many :notes, serializer: CustomNoteSerializer
54
+ attributes(:last_name, :created_at, :updated_at)
55
+
56
+ attribute :first_name do |object, params|
57
+ if params[:first_name_upcase]
58
+ object.first_name.upcase
59
+ else
60
+ object.first_name
61
+ end
62
+ end
63
+ end
64
+
65
+ class Dummy < Rails::Application
66
+ secrets.secret_key_base = '_'
67
+ config.hosts << 'www.example.com' if config.respond_to?(:hosts)
68
+
69
+ routes.draw do
70
+ scope defaults: { format: :jsonapi } do
71
+ resources :users, only: [:index]
72
+ resources :notes, only: [:update]
73
+ end
74
+ end
75
+ end
76
+
77
+ class UsersController < ActionController::Base
78
+ include JSONAPI::Fetching
79
+ include JSONAPI::Filtering
80
+ include JSONAPI::Pagination
81
+ include JSONAPI::Deserialization
82
+
83
+ def index
84
+ allowed_fields = [
85
+ :first_name, :last_name, :created_at,
86
+ :notes_created_at, :notes_quantity
87
+ ]
88
+ options = { sort_with_expressions: true }
89
+
90
+ jsonapi_filter(User.all, allowed_fields, options) do |filtered|
91
+ result = filtered.result
92
+
93
+ if params[:sort].to_s.include?('notes_quantity')
94
+ render jsonapi: result.group('id').to_a
95
+ return
96
+ end
97
+
98
+ result = result.to_a if params[:as_list]
99
+
100
+ jsonapi_paginate(result) do |paginated|
101
+ render jsonapi: paginated
102
+ end
103
+ end
104
+ end
105
+
106
+ private
107
+ def jsonapi_meta(resources)
108
+ {
109
+ many: true,
110
+ pagination: jsonapi_pagination_meta(resources)
111
+ }
112
+ end
113
+
114
+ def jsonapi_serializer_params
115
+ {
116
+ first_name_upcase: params[:upcase]
117
+ }
118
+ end
119
+ end
120
+
121
+ class NotesController < ActionController::Base
122
+ include JSONAPI::Errors
123
+ include JSONAPI::Deserialization
124
+
125
+ def update
126
+ raise_error! if params[:id] == 'tada'
127
+
128
+ note = Note.find(params[:id])
129
+
130
+ if note.update(note_params)
131
+ render jsonapi: note
132
+ else
133
+ note.errors.add(:title, message: 'has typos') if note.errors.key?(:title)
134
+
135
+ render jsonapi_errors: note.errors, status: :unprocessable_entity
136
+ end
137
+ end
138
+
139
+ private
140
+ def render_jsonapi_internal_server_error(exception)
141
+ Rails.logger.error(exception)
142
+ super(exception)
143
+ end
144
+
145
+ def jsonapi_serializer_class(resource, is_collection)
146
+ JSONAPI::Rails.serializer_class(resource, is_collection)
147
+ rescue NameError
148
+ klass = resource.class
149
+ klass = resource.first.class if is_collection
150
+ "Custom#{klass.name}Serializer".constantize
151
+ end
152
+
153
+ def note_params
154
+ # Will trigger required attribute error handling
155
+ params.require(:data).require(:attributes).require(:title)
156
+
157
+ jsonapi_deserialize(params)
158
+ end
159
+
160
+ def jsonapi_meta(resources)
161
+ { single: true }
162
+ end
163
+ end
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NotesController, type: :request do
4
+ describe 'PUT /notes/:id' do
5
+ let(:note) { create_note }
6
+ let(:note_id) { note.id }
7
+ let(:user) { note.user }
8
+ let(:user_id) { user.id }
9
+ let(:note_params) do
10
+ {
11
+ data: {
12
+ attributes: { title: FFaker::Company.name },
13
+ relationships: { user: { data: { id: user_id } } }
14
+ }
15
+ }
16
+ end
17
+ let(:params) { note_params }
18
+
19
+ before do
20
+ put(note_path(note_id), params: params.to_json, headers: jsonapi_headers)
21
+ end
22
+
23
+ it do
24
+ expect(response).to have_http_status(:ok)
25
+ expect(response_json['data']).to have_id(note.id.to_s)
26
+ expect(response_json['meta']).to eq('single' => true)
27
+ end
28
+
29
+ context 'with a missing parameter in the payload' do
30
+ let(:params) { {} }
31
+
32
+ it do
33
+ expect(response).to have_http_status(:unprocessable_entity)
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
40
+ end
41
+ end
42
+
43
+ context 'with an invalid payload' do
44
+ let(:params) do
45
+ payload = note_params.dup
46
+ payload[:data][:relationships][:user][:data][:id] = nil
47
+ payload
48
+ end
49
+
50
+ it do
51
+ expect(response).to have_http_status(:unprocessable_entity)
52
+ 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::VERSION::MAJOR >= 6 && Rails::VERSION::MINOR >= 1
60
+ expect(response_json['errors'][0]['detail'])
61
+ .to eq('User must exist')
62
+ else
63
+ expect(response_json['errors'][0]['detail'])
64
+ .to eq('User can\'t be blank')
65
+ end
66
+ end
67
+
68
+ context 'required by validations' do
69
+ let(:params) do
70
+ payload = note_params.dup
71
+ payload[:data][:attributes][:title] = 'BAD_TITLE'
72
+ payload[:data][:attributes][:quantity] = 100 + rand(10)
73
+ payload
74
+ end
75
+
76
+ it do
77
+ expect(response).to have_http_status(:unprocessable_entity)
78
+ 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')
118
+ end
119
+ end
120
+
121
+ context 'as a param attribute' do
122
+ let(:params) do
123
+ payload = note_params.dup
124
+ payload[:data][:attributes].delete(:title)
125
+ # To have any attribtues in the payload...
126
+ payload[:data][:attributes][:created_at] = nil
127
+ payload
128
+ end
129
+
130
+ it do
131
+ expect(response).to have_http_status(:unprocessable_entity)
132
+ expect(response_json['errors'][0]['source'])
133
+ .to eq('pointer' => '/data/attributes/title')
134
+ end
135
+ end
136
+ end
137
+
138
+ context 'with a bad note ID' do
139
+ let(:user_id) { nil }
140
+ let(:note_id) { rand(10) }
141
+
142
+ it do
143
+ expect(response).to have_http_status(:not_found)
144
+ 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
150
+ end
151
+ end
152
+
153
+ context 'with an exception' do
154
+ let(:user_id) { nil }
155
+ let(:note_id) { 'tada' }
156
+
157
+ it do
158
+ expect(response).to have_http_status(:internal_server_error)
159
+ 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
165
+ end
166
+ end
167
+ end
168
+ end