jsonapi.rb 1.5.7 → 2.0.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
- 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