jsonapi.rb 1.6.0 → 1.7.0

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: 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