jsonapi.rb 1.5.7 → 2.0.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
- SHA1:
3
- metadata.gz: 521d608a58983b2c99246e34faf27eae9bb38b32
4
- data.tar.gz: 02a2e7a0086ede91a907d8c7d097150f0222b768
2
+ SHA256:
3
+ metadata.gz: 2192bbffc57d4ecbe419a90e25728951db4464c5f299e8866afefe72fa393cad
4
+ data.tar.gz: f7f83b43169207e7cf6d06e6e99cc8c8f705de09c4c83ff9dba5238540577a9e
5
5
  SHA512:
6
- metadata.gz: 23f3ebb21b586e5995b6cb4f2c2ae8f3636f93f5f0e11b9fdbbabdb4087c32850bcda172aeda4aabb8f58f762a23e8c034128f4b48bb7ed38d5ee2c573778ff4
7
- data.tar.gz: b0cdb369d4265dd306a1af8206a46f837322ec00142ec2fac9c40c0ebcc6010cd4f2b7982e05f7ff2f489a960def72431ffc140f3460bffcf9ebecf65aee710d
6
+ metadata.gz: 577c2b0d1801ef2191c25925e407449ff12ba95d7fc5c8433fcbb2b20e532a1b7a51a884f147c5042c38ec27ca9eed6567bfd746aad4ad8895b7a3f90e9043b1
7
+ data.tar.gz: f6ca1bc49fd89c1ea942f3c047234d76c4fa1d7604bd06aa5d67c43dad93603cb19e6c542ec13837d1db5e3fbe923ec4799fd865c96c08822dcab7cb160d7f7e
data/README.md CHANGED
@@ -31,7 +31,7 @@ Main goals:
31
31
 
32
32
  The available features include:
33
33
 
34
- * object serialization (powered by Fast JSON API)
34
+ * object serialization (powered by JSON:API Serializer, was `fast_jsonapi`)
35
35
  * [error handling](https://jsonapi.org/format/#errors) (parameters,
36
36
  validation, generic errors)
37
37
  * fetching of the data (support for
@@ -39,16 +39,26 @@ The available features include:
39
39
  [sparse fields](https://jsonapi.org/format/#fetching-sparse-fieldsets))
40
40
  * [filtering](https://jsonapi.org/format/#fetching-filtering) and
41
41
  [sorting](https://jsonapi.org/format/#fetching-sorting) of the data
42
- (powered by Ransack)
42
+ (powered by Ransack, soft-dependency)
43
43
  * [pagination](https://jsonapi.org/format/#fetching-pagination) support
44
44
 
45
45
  ## But how?
46
46
 
47
- Mainly by leveraging [Fast JSON API](https://github.com/Netflix/fast_jsonapi)
47
+ Mainly by leveraging [JSON:API Serializer](https://github.com/jsonapi-serializer/jsonapi-serializer)
48
48
  and [Ransack](https://github.com/activerecord-hackery/ransack).
49
49
 
50
50
  Thanks to everyone who worked on these amazing projects!
51
51
 
52
+ ## Sponsors
53
+
54
+ I'm grateful for the following companies for supporting this project!
55
+
56
+ <p align="center">
57
+ <a href="https://www.luneteyewear.com"><img src="https://user-images.githubusercontent.com/112147/136836142-2bfba96e-447f-4eb6-b137-2445aee81b37.png"/></a>
58
+ <a href="https://www.startuplandia.io"><img src="https://user-images.githubusercontent.com/112147/136836147-93f8ab17-2465-4477-a7ab-e38255483c66.png"/></a>
59
+ </p>
60
+
61
+
52
62
  ## Installation
53
63
 
54
64
  Add this line to your application's Gemfile:
@@ -100,7 +110,7 @@ The naming scheme follows the `ModuleName::ClassNameSerializer` for an instance
100
110
  of the `ModuleName::ClassName`.
101
111
 
102
112
  Please follow the
103
- [Fast JSON API guide](https://github.com/Netflix/fast_jsonapi#serializer-definition)
113
+ [JSON:API Serializer guide](https://github.com/jsonapi-serializer/jsonapi-serializer#serializer-definition)
104
114
  on how to define a serializer.
105
115
 
106
116
  To provide a different naming scheme implement the `jsonapi_serializer_class`
@@ -231,6 +241,8 @@ to filter and sort over a collection of records.
231
241
  The support is pretty extended and covers also relationships and composite
232
242
  matchers.
233
243
 
244
+ Please add `ransack` to your `Gemfile` in order to benefit from this functionality!
245
+
234
246
  Here's an example:
235
247
 
236
248
  ```ruby
@@ -290,6 +302,7 @@ class MyController < ActionController::Base
290
302
  render jsonapi: paginated
291
303
  end
292
304
  end
305
+
293
306
  end
294
307
  ```
295
308
 
@@ -306,6 +319,17 @@ use the `jsonapi_pagination_meta` method:
306
319
  end
307
320
 
308
321
  ```
322
+
323
+ If you want to change the default number of items per page or define a custom logic to handle page size, use the
324
+ `jsonapi_page_size` method:
325
+
326
+ ```ruby
327
+ def jsonapi_page_size(pagination_params)
328
+ per_page = pagination_params[:size].to_f.to_i
329
+ per_page = 30 if per_page > 30 || per_page < 1
330
+ per_page
331
+ end
332
+ ```
309
333
  ### Deserialization
310
334
 
311
335
  `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
@@ -1,11 +1,10 @@
1
- require 'fast_jsonapi'
1
+ require 'jsonapi/serializer'
2
2
 
3
3
  module JSONAPI
4
4
  # A simple error serializer
5
5
  class ErrorSerializer
6
- include FastJsonapi::ObjectSerializer
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]
@@ -1,5 +1,10 @@
1
- require 'ransack/predicate'
2
- require_relative 'patches'
1
+ begin
2
+ require 'active_record'
3
+ require 'ransack'
4
+ require_relative 'patches'
5
+ rescue LoadError
6
+ warn('Install `ransack` gem before using `JSONAPI::Filtering`!')
7
+ end
3
8
 
4
9
  # Filtering and sorting support
5
10
  module JSONAPI
@@ -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
@@ -42,11 +42,12 @@ module JSONAPI
42
42
  many = JSONAPI::Rails.is_collection?(resource, options[:is_collection])
43
43
  resource = [resource] unless many
44
44
 
45
- return JSONAPI::ErrorSerializer.new(resource, options)
46
- .serialized_json unless resource.is_a?(ActiveModel::Errors)
45
+ return JSONAPI::Rails.serializer_to_json(
46
+ JSONAPI::ErrorSerializer.new(resource, options)
47
+ ) unless resource.is_a?(ActiveModel::Errors)
47
48
 
48
49
  errors = []
49
- model = resource.instance_variable_get('@base')
50
+ model = resource.instance_variable_get(:@base)
50
51
 
51
52
  if respond_to?(:jsonapi_serializer_class, true)
52
53
  model_serializer = jsonapi_serializer_class(model, false)
@@ -54,8 +55,18 @@ module JSONAPI
54
55
  model_serializer = JSONAPI::Rails.serializer_class(model, false)
55
56
  end
56
57
 
57
- details = resource.messages
58
- details = resource.details if resource.respond_to?(:details)
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
59
70
 
60
71
  details.each do |error_key, error_hashes|
61
72
  error_hashes.each do |error_hash|
@@ -66,9 +77,11 @@ module JSONAPI
66
77
  end
67
78
  end
68
79
 
69
- JSONAPI::ActiveModelErrorSerializer.new(
70
- errors, params: { model: model, model_serializer: model_serializer }
71
- ).serialized_json
80
+ JSONAPI::Rails.serializer_to_json(
81
+ JSONAPI::ActiveModelErrorSerializer.new(
82
+ errors, params: { model: model, model_serializer: model_serializer }
83
+ )
84
+ )
72
85
  end
73
86
  end
74
87
 
@@ -100,21 +113,21 @@ module JSONAPI
100
113
  serializer_class = JSONAPI::Rails.serializer_class(resource, many)
101
114
  end
102
115
 
103
- serializer_class.new(resource, options).serialized_json
116
+ JSONAPI::Rails.serializer_to_json(
117
+ serializer_class.new(resource, options)
118
+ )
104
119
  end
105
120
  end
106
121
 
107
122
  # Checks if an object is a collection
108
123
  #
109
- # Stolen from [FastJsonapi::ObjectSerializer], instance method.
124
+ # Basically forwards it to a [JSONAPI::Serializer] as there's no public API
110
125
  #
111
126
  # @param resource [Object] to check
112
127
  # @param force_is_collection [NilClass] flag to overwrite
113
128
  # @return [TrueClass] upon success
114
129
  def self.is_collection?(resource, force_is_collection = nil)
115
- return force_is_collection unless force_is_collection.nil?
116
-
117
- resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
130
+ JSONAPI::ErrorSerializer.is_collection?(resource, force_is_collection)
118
131
  end
119
132
 
120
133
  # Resolves resource serializer class
@@ -126,5 +139,17 @@ module JSONAPI
126
139
 
127
140
  "#{klass.name}Serializer".constantize
128
141
  end
142
+
143
+ # Lazily returns the serializer JSON
144
+ #
145
+ # @param serializer [Object] to evaluate
146
+ # @return [String]
147
+ def self.serializer_to_json(serializer)
148
+ if serializer.respond_to?(:serialized_json)
149
+ serializer.serialized_json
150
+ else
151
+ serializer.serializable_hash.to_json
152
+ end
153
+ end
129
154
  end
130
155
  end
@@ -1,3 +1,3 @@
1
1
  module JSONAPI
2
- VERSION = '1.5.7'
2
+ VERSION = '2.0.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,162 @@
1
+ require 'securerandom'
2
+ require 'rails/all'
3
+ require 'ransack'
4
+ require 'jsonapi'
5
+
6
+ Rails.logger = Logger.new(STDOUT)
7
+ Rails.logger.level = ENV['LOG_LEVEL'] || Logger::WARN
8
+
9
+ JSONAPI::Rails.install!
10
+
11
+ ActiveRecord::Base.logger = Rails.logger
12
+ ActiveRecord::Base.establish_connection(
13
+ ENV['DATABASE_URL'] || 'sqlite3::memory:'
14
+ )
15
+
16
+ ActiveRecord::Schema.define do
17
+ create_table :users, force: true do |t|
18
+ t.string :first_name
19
+ t.string :last_name
20
+ t.timestamps
21
+ end
22
+
23
+ create_table :notes, force: true do |t|
24
+ t.string :title
25
+ t.integer :user_id
26
+ t.integer :quantity
27
+ t.timestamps
28
+ end
29
+ end
30
+
31
+ class User < ActiveRecord::Base
32
+ has_many :notes
33
+ end
34
+
35
+ class Note < ActiveRecord::Base
36
+ validates_format_of :title, without: /BAD_TITLE/
37
+ validates_numericality_of :quantity, less_than: 100, if: :quantity?
38
+ belongs_to :user, required: true
39
+ end
40
+
41
+ class CustomNoteSerializer
42
+ include JSONAPI::Serializer
43
+
44
+ set_type :note
45
+ belongs_to :user
46
+ attributes(:title, :quantity, :created_at, :updated_at)
47
+ end
48
+
49
+ class UserSerializer
50
+ include JSONAPI::Serializer
51
+
52
+ has_many :notes, serializer: CustomNoteSerializer
53
+ attributes(:last_name, :created_at, :updated_at)
54
+
55
+ attribute :first_name do |object, params|
56
+ if params[:first_name_upcase]
57
+ object.first_name.upcase
58
+ else
59
+ object.first_name
60
+ end
61
+ end
62
+ end
63
+
64
+ class Dummy < Rails::Application
65
+ secrets.secret_key_base = '_'
66
+ config.hosts << 'www.example.com' if config.respond_to?(:hosts)
67
+
68
+ routes.draw do
69
+ scope defaults: { format: :jsonapi } do
70
+ resources :users, only: [:index]
71
+ resources :notes, only: [:update]
72
+ end
73
+ end
74
+ end
75
+
76
+ class UsersController < ActionController::Base
77
+ include JSONAPI::Fetching
78
+ include JSONAPI::Filtering
79
+ include JSONAPI::Pagination
80
+ include JSONAPI::Deserialization
81
+
82
+ def index
83
+ allowed_fields = [
84
+ :first_name, :last_name, :created_at,
85
+ :notes_created_at, :notes_quantity
86
+ ]
87
+ options = { sort_with_expressions: true }
88
+
89
+ jsonapi_filter(User.all, allowed_fields, options) do |filtered|
90
+ result = filtered.result
91
+
92
+ if params[:sort].to_s.include?('notes_quantity')
93
+ render jsonapi: result.group('id').to_a
94
+ return
95
+ end
96
+
97
+ result = result.to_a if params[:as_list]
98
+
99
+ jsonapi_paginate(result) do |paginated|
100
+ render jsonapi: paginated
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+ def jsonapi_meta(resources)
107
+ {
108
+ many: true,
109
+ pagination: jsonapi_pagination_meta(resources)
110
+ }
111
+ end
112
+
113
+ def jsonapi_serializer_params
114
+ {
115
+ first_name_upcase: params[:upcase]
116
+ }
117
+ end
118
+ end
119
+
120
+ class NotesController < ActionController::Base
121
+ include JSONAPI::Errors
122
+ include JSONAPI::Deserialization
123
+
124
+ def update
125
+ raise_error! if params[:id] == 'tada'
126
+
127
+ note = Note.find(params[:id])
128
+
129
+ if note.update(note_params)
130
+ render jsonapi: note
131
+ else
132
+ note.errors.add(:title, message: 'has typos') if note.errors.key?(:title)
133
+
134
+ render jsonapi_errors: note.errors, status: :unprocessable_entity
135
+ end
136
+ end
137
+
138
+ private
139
+ def render_jsonapi_internal_server_error(exception)
140
+ Rails.logger.error(exception)
141
+ super(exception)
142
+ end
143
+
144
+ def jsonapi_serializer_class(resource, is_collection)
145
+ JSONAPI::Rails.serializer_class(resource, is_collection)
146
+ rescue NameError
147
+ klass = resource.class
148
+ klass = resource.first.class if is_collection
149
+ "Custom#{klass.name}Serializer".constantize
150
+ end
151
+
152
+ def note_params
153
+ # Will trigger required attribute error handling
154
+ params.require(:data).require(:attributes).require(:title)
155
+
156
+ jsonapi_deserialize(params)
157
+ end
158
+
159
+ def jsonapi_meta(resources)
160
+ { single: true }
161
+ end
162
+ end