jsonapi.rb 1.1.0 → 1.1.1
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 +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +1 -1
- data/jsonapi.rb.gemspec +1 -1
- data/lib/jsonapi/errors.rb +34 -32
- data/lib/jsonapi/fetching.rb +22 -20
- data/lib/jsonapi/filtering.rb +59 -57
- data/lib/jsonapi/pagination.rb +72 -70
- data/lib/jsonapi/rails.rb +78 -76
- data/lib/jsonapi/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c76a1e2a0107283282044d238d2926cf55e0722a0a4fe263035af40f30a8da8
|
4
|
+
data.tar.gz: c178cbd58f5d842400db948830eca9f5a784d3ade02bce30f048484e0d5d2178
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 104169038398545873c56698b765ad9f7c91f4694c61568531ea82d5315c395956e3548ec769df03c7d3b01ade8a1966916a324af3b2a236d1afd237d81c1908
|
7
|
+
data.tar.gz: 39bbeb7ae0205139c41a6ba5010369694cbb1d0d9ca902a5830a7e911d3e0ca6945879aa370bf64d44322bfd8971d9b88328d1b97c29e9d68029eb9544aa7ef8
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
data/jsonapi.rb.gemspec
CHANGED
data/lib/jsonapi/errors.rb
CHANGED
@@ -1,45 +1,47 @@
|
|
1
1
|
require 'net/http/status'
|
2
2
|
require 'active_support/concern'
|
3
3
|
|
4
|
-
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
|
9
|
-
|
4
|
+
module JSONAPI
|
5
|
+
# Helpers to handle some error responses
|
6
|
+
#
|
7
|
+
# Most of the exceptions are handled in Rails by [ActionDispatch] middleware
|
8
|
+
# See: https://api.rubyonrails.org/classes/ActionDispatch/ExceptionWrapper.html
|
9
|
+
module Errors
|
10
|
+
extend ActiveSupport::Concern
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
included do
|
13
|
+
rescue_from StandardError do |exception|
|
14
|
+
error = { status: '500', title: Net::HTTP::STATUS_CODES[500] }
|
15
|
+
render jsonapi_errors: [error], status: :internal_server_error
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
[
|
19
|
+
ActiveRecord::RecordNotFound
|
20
|
+
].each do |exception_class|
|
21
|
+
rescue_from exception_class do |exception|
|
22
|
+
error = { status: '404', title: Net::HTTP::STATUS_CODES[404] }
|
23
|
+
render jsonapi_errors: [error], status: :not_found
|
24
|
+
end
|
23
25
|
end
|
24
|
-
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
[
|
28
|
+
ActionController::ParameterMissing
|
29
|
+
].each do |exception_class|
|
30
|
+
rescue_from exception_class do |exception|
|
31
|
+
source = { pointer: '' }
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
if !%w{data attributes relationships}.include?(exception.param.to_s)
|
34
|
+
source[:pointer] = "/data/attributes/#{exception.param}"
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
error = {
|
38
|
+
status: '422',
|
39
|
+
title: Net::HTTP::STATUS_CODES[422],
|
40
|
+
source: source
|
41
|
+
}
|
41
42
|
|
42
|
-
|
43
|
+
render jsonapi_errors: [error], status: :unprocessable_entity
|
44
|
+
end
|
43
45
|
end
|
44
46
|
end
|
45
47
|
end
|
data/lib/jsonapi/fetching.rb
CHANGED
@@ -1,26 +1,28 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module JSONAPI
|
2
|
+
# Inclusion and sparse fields support
|
3
|
+
module Fetching
|
4
|
+
private
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
# Extracts and formats sparse fieldsets
|
7
|
+
#
|
8
|
+
# Ex.: `GET /resource?fields[relationship]=id,created_at`
|
9
|
+
#
|
10
|
+
# @return [Hash]
|
11
|
+
def jsonapi_fields
|
12
|
+
ActiveSupport::HashWithIndifferentAccess.new.tap do |h|
|
13
|
+
(params[:fields] || []).each do |k, v|
|
14
|
+
h[k] = v.split(',').map(&:strip).compact
|
15
|
+
end
|
14
16
|
end
|
15
17
|
end
|
16
|
-
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
# Extracts and whitelists allowed includes
|
20
|
+
#
|
21
|
+
# Ex.: `GET /resource?include=relationship,relationship.subrelationship`
|
22
|
+
#
|
23
|
+
# @return [Array]
|
24
|
+
def jsonapi_include
|
25
|
+
params['include'].to_s.split(',').map(&:strip).compact
|
26
|
+
end
|
25
27
|
end
|
26
28
|
end
|
data/lib/jsonapi/filtering.rb
CHANGED
@@ -1,73 +1,75 @@
|
|
1
1
|
require 'ransack/predicate'
|
2
2
|
|
3
3
|
# Filtering and sorting support
|
4
|
-
module JSONAPI
|
5
|
-
|
4
|
+
module JSONAPI
|
5
|
+
module Filtering
|
6
|
+
private
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
8
|
+
# Applies filtering and sorting to a set of resources if requested
|
9
|
+
#
|
10
|
+
# The fields follow [Ransack] specifications.
|
11
|
+
# See: https://github.com/activerecord-hackery/ransack#search-matchers
|
12
|
+
#
|
13
|
+
# Ex.: `GET /resource?filter[region_matches_any]=Lisb%&sort=-created_at,id`
|
14
|
+
#
|
15
|
+
# @param allowed_fields [Array] a list of allowed fields to be filtered
|
16
|
+
# @return [ActiveRecord::Base] a collection of resources
|
17
|
+
def jsonapi_filter(resources, allowed_fields)
|
18
|
+
extracted_params = jsonapi_filter_params(allowed_fields)
|
19
|
+
extracted_params[:sorts] = jsonapi_sort_params(allowed_fields)
|
20
|
+
resources = resources.ransack(extracted_params)
|
21
|
+
block_given? ? yield(resources) : resources
|
22
|
+
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
24
|
+
# Extracts and whitelists allowed fields to be filtered
|
25
|
+
#
|
26
|
+
# The fields follow [Ransack] specifications.
|
27
|
+
# See: https://github.com/activerecord-hackery/ransack#search-matchers
|
28
|
+
#
|
29
|
+
# @param allowed_fields [Array] a list of allowed fields to be filtered
|
30
|
+
# @return [Hash] to be passed to [ActiveRecord::Base#order]
|
31
|
+
def jsonapi_filter_params(allowed_fields)
|
32
|
+
filtered = {}
|
33
|
+
requested = params[:filter] || {}
|
34
|
+
allowed_fields = allowed_fields.map(&:to_s)
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
requested.each_pair do |requested_field, to_filter|
|
37
|
+
field_name = requested_field.dup
|
38
|
+
predicate = Ransack::Predicate.detect_and_strip_from_string!(field_name)
|
39
|
+
predicate = Ransack::Predicate.named(predicate)
|
39
40
|
|
40
|
-
|
41
|
+
field_names = field_name.split(/_and_|_or_/)
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
if to_filter.is_a?(String) && to_filter.include?(',')
|
44
|
+
to_filter = to_filter.split(',')
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
47
|
+
if predicate && (field_names - allowed_fields).empty?
|
48
|
+
filtered[requested_field] = to_filter
|
49
|
+
end
|
48
50
|
end
|
49
|
-
end
|
50
51
|
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
# Extracts and whitelists allowed fields to be sorted
|
55
|
-
#
|
56
|
-
# @param allowed_fields [Array] a list of allowed fields to be sorted
|
57
|
-
# @return [Hash] to be passed to [ActiveRecord::Base#order]
|
58
|
-
def jsonapi_sort_params(allowed_fields)
|
59
|
-
requested = params[:sort].to_s.split(',')
|
60
|
-
requested.map! do |requested_field|
|
61
|
-
desc = requested_field.to_s.start_with?('-')
|
62
|
-
[
|
63
|
-
desc ? requested_field[1..-1] : requested_field,
|
64
|
-
desc ? 'desc' : 'asc'
|
65
|
-
]
|
52
|
+
filtered
|
66
53
|
end
|
67
54
|
|
68
|
-
#
|
69
|
-
|
70
|
-
|
55
|
+
# Extracts and whitelists allowed fields to be sorted
|
56
|
+
#
|
57
|
+
# @param allowed_fields [Array] a list of allowed fields to be sorted
|
58
|
+
# @return [Hash] to be passed to [ActiveRecord::Base#order]
|
59
|
+
def jsonapi_sort_params(allowed_fields)
|
60
|
+
requested = params[:sort].to_s.split(',')
|
61
|
+
requested.map! do |requested_field|
|
62
|
+
desc = requested_field.to_s.start_with?('-')
|
63
|
+
[
|
64
|
+
desc ? requested_field[1..-1] : requested_field,
|
65
|
+
desc ? 'desc' : 'asc'
|
66
|
+
]
|
67
|
+
end
|
68
|
+
|
69
|
+
# Convert to strings instead of hashes to allow joined table columns.
|
70
|
+
requested.to_h.slice(*allowed_fields.map(&:to_s)).map do |field, dir|
|
71
|
+
[field, dir].join(' ')
|
72
|
+
end
|
71
73
|
end
|
72
74
|
end
|
73
75
|
end
|
data/lib/jsonapi/pagination.rb
CHANGED
@@ -1,91 +1,93 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
1
|
+
module JSONAPI
|
2
|
+
# Pagination support
|
3
|
+
module Pagination
|
4
|
+
private
|
5
|
+
|
6
|
+
# Default number of items per page.
|
7
|
+
JSONAPI_PAGE_SIZE = 30
|
8
|
+
|
9
|
+
# Applies pagination to a set of resources
|
10
|
+
#
|
11
|
+
# Ex.: `GET /resource?page[number]=2&page[size]=10`
|
12
|
+
#
|
13
|
+
# @return [ActiveRecord::Base] a collection of resources
|
14
|
+
def jsonapi_paginate(resources)
|
15
|
+
offset, limit, _ = jsonapi_pagination_params
|
16
|
+
|
17
|
+
if resources.respond_to?(:offset)
|
18
|
+
resources = resources.offset(offset).limit(limit)
|
19
|
+
else
|
20
|
+
resources = resources[(offset)..(offset + limit)]
|
21
|
+
end
|
22
|
+
|
23
|
+
block_given? ? yield(resources) : resources
|
20
24
|
end
|
21
25
|
|
22
|
-
|
23
|
-
|
26
|
+
# Generates the pagination links
|
27
|
+
#
|
28
|
+
# @return [Array]
|
29
|
+
def jsonapi_pagination(resources)
|
30
|
+
links = { self: request.base_url + request.original_fullpath }
|
31
|
+
pagination = jsonapi_pagination_meta(resources)
|
24
32
|
|
25
|
-
|
26
|
-
#
|
27
|
-
# @return [Array]
|
28
|
-
def jsonapi_pagination(resources)
|
29
|
-
links = { self: request.base_url + request.original_fullpath }
|
30
|
-
pagination = jsonapi_pagination_meta(resources)
|
33
|
+
return links if pagination.blank?
|
31
34
|
|
32
|
-
|
35
|
+
original_params = params.except(
|
36
|
+
*request.path_parameters.keys.map(&:to_s)).to_unsafe_h
|
37
|
+
original_params[:page] ||= {}
|
38
|
+
original_url = request.base_url + request.path + '?'
|
33
39
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
pagination.each do |page_name, number|
|
41
|
+
original_params[:page][:number] = number
|
42
|
+
links[page_name] = original_url + CGI.unescape(original_params.to_query)
|
43
|
+
end
|
38
44
|
|
39
|
-
|
40
|
-
original_params[:page][:number] = number
|
41
|
-
links[page_name] = original_url + CGI.unescape(original_params.to_query)
|
45
|
+
links
|
42
46
|
end
|
43
47
|
|
44
|
-
|
45
|
-
|
48
|
+
# Generates pagination numbers
|
49
|
+
#
|
50
|
+
# @return [Hash] with the first, previous, next, current and last page number
|
51
|
+
def jsonapi_pagination_meta(resources)
|
52
|
+
return {} unless JSONAPI::Rails.is_collection?(resources)
|
46
53
|
|
47
|
-
|
48
|
-
#
|
49
|
-
# @return [Hash] with the first, previous, next, current and last page number
|
50
|
-
def jsonapi_pagination_meta(resources)
|
51
|
-
return {} unless JSONAPI::Rails.is_collection?(resources)
|
54
|
+
_, limit, page = jsonapi_pagination_params
|
52
55
|
|
53
|
-
|
56
|
+
numbers = { current: page }
|
54
57
|
|
55
|
-
|
58
|
+
if resources.respond_to?(:unscope)
|
59
|
+
total = resources.unscope(:limit, :offset).count()
|
60
|
+
else
|
61
|
+
total = resources.size
|
62
|
+
end
|
56
63
|
|
57
|
-
|
58
|
-
total = resources.unscope(:limit, :offset).count()
|
59
|
-
else
|
60
|
-
total = resources.size
|
61
|
-
end
|
64
|
+
last_page = [1, (total.to_f / limit).ceil].max
|
62
65
|
|
63
|
-
|
66
|
+
if page > 1
|
67
|
+
numbers[:first] = 1
|
68
|
+
numbers[:prev] = page - 1
|
69
|
+
end
|
64
70
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
71
|
+
if page < last_page
|
72
|
+
numbers[:next] = page + 1
|
73
|
+
numbers[:last] = last_page
|
74
|
+
end
|
69
75
|
|
70
|
-
|
71
|
-
numbers[:next] = page + 1
|
72
|
-
numbers[:last] = last_page
|
76
|
+
numbers
|
73
77
|
end
|
74
78
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
# @return [Array] with the offset, limit and the current page number
|
81
|
-
def jsonapi_pagination_params
|
82
|
-
def_per_page = self.class.const_get(:JSONAPI_PAGE_SIZE).to_i
|
79
|
+
# Extracts the pagination params
|
80
|
+
#
|
81
|
+
# @return [Array] with the offset, limit and the current page number
|
82
|
+
def jsonapi_pagination_params
|
83
|
+
def_per_page = self.class.const_get(:JSONAPI_PAGE_SIZE).to_i
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
85
|
+
pagination = params[:page].try(:slice, :number, :size) || {}
|
86
|
+
per_page = (pagination[:size] || def_per_page).to_f.to_i
|
87
|
+
per_page = def_per_page if per_page > def_per_page
|
88
|
+
num = [1, pagination[:number].to_f.to_i].max
|
88
89
|
|
89
|
-
|
90
|
+
[(num - 1) * per_page, per_page, num]
|
91
|
+
end
|
90
92
|
end
|
91
93
|
end
|
data/lib/jsonapi/rails.rb
CHANGED
@@ -2,97 +2,99 @@ require 'jsonapi/error_serializer'
|
|
2
2
|
require 'jsonapi/active_model_error_serializer'
|
3
3
|
|
4
4
|
# Rails integration
|
5
|
-
module JSONAPI
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
5
|
+
module JSONAPI
|
6
|
+
module Rails
|
7
|
+
# Updates the mime types and registers the renderers
|
8
|
+
#
|
9
|
+
# @return [NilClass]
|
10
|
+
def self.install!
|
11
|
+
return unless defined?(::Rails)
|
12
|
+
|
13
|
+
Mime::Type.register JSONAPI::MEDIA_TYPE, :jsonapi
|
14
|
+
|
15
|
+
# Map the JSON parser to the JSONAPI mime type requests.
|
16
|
+
if Rails::VERSION::MAJOR >= 5
|
17
|
+
parser = ActionDispatch::Request.parameter_parsers[:json]
|
18
|
+
ActionDispatch::Request.parameter_parsers[:jsonapi] = parser
|
19
|
+
else
|
20
|
+
parser = ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:json]]
|
21
|
+
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = parser
|
22
|
+
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
self.add_renderer!
|
25
|
+
self.add_errors_renderer!
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
# Adds the error renderer
|
29
|
+
#
|
30
|
+
# @return [NilClass]
|
31
|
+
def self.add_errors_renderer!
|
32
|
+
ActionController::Renderers.add(:jsonapi_errors) do |resource, options|
|
33
|
+
self.content_type ||= Mime[:jsonapi]
|
33
34
|
|
34
|
-
|
35
|
+
resource = [resource] unless JSONAPI::Rails.is_collection?(resource)
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
return JSONAPI::ErrorSerializer.new(resource, options)
|
38
|
+
.serialized_json unless resource.is_a?(ActiveModel::Errors)
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
errors = []
|
41
|
+
model = resource.marshal_dump.first
|
42
|
+
model_serializer = JSONAPI::Rails.serializer_class(model)
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
resource.details.each do |error_key, error_hashes|
|
45
|
+
error_hashes.each do |error_hash|
|
46
|
+
errors << [ error_key, error_hash ]
|
47
|
+
end
|
46
48
|
end
|
47
|
-
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
JSONAPI::ActiveModelErrorSerializer.new(
|
51
|
+
errors, params: { model: model, model_serializer: model_serializer }
|
52
|
+
).serialized_json
|
53
|
+
end
|
52
54
|
end
|
53
|
-
end
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
56
|
+
# Adds the default renderer
|
57
|
+
#
|
58
|
+
# @return [NilClass]
|
59
|
+
def self.add_renderer!
|
60
|
+
ActionController::Renderers.add(:jsonapi) do |resource, options|
|
61
|
+
self.content_type ||= Mime[:jsonapi]
|
62
|
+
|
63
|
+
options[:meta] ||= (
|
64
|
+
jsonapi_meta(resource) if respond_to?(:jsonapi_meta, true))
|
65
|
+
options[:links] ||= (
|
66
|
+
jsonapi_pagination(resource) if respond_to?(:jsonapi_pagination, true))
|
67
|
+
|
68
|
+
# If it's an empty collection, return it directly.
|
69
|
+
if JSONAPI::Rails.is_collection?(resource) && !resource.any?
|
70
|
+
return options.slice(:meta, :links).merge(data: []).to_json
|
71
|
+
end
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
73
|
+
options[:fields] ||= jsonapi_fields if respond_to?(:jsonapi_fields, true)
|
74
|
+
options[:include] ||= (
|
75
|
+
jsonapi_include if respond_to?(:jsonapi_include, true))
|
75
76
|
|
76
|
-
|
77
|
-
|
77
|
+
serializer_class = JSONAPI::Rails.serializer_class(resource)
|
78
|
+
serializer_class.new(resource, options).serialized_json
|
79
|
+
end
|
78
80
|
end
|
79
|
-
end
|
80
81
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
82
|
+
# Checks if an object is a collection
|
83
|
+
#
|
84
|
+
# @param object [Object] to check
|
85
|
+
# @return [TrueClass] upon success
|
86
|
+
def self.is_collection?(object)
|
87
|
+
object.is_a?(Enumerable) && !object.respond_to?(:each_pair)
|
88
|
+
end
|
88
89
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
90
|
+
# Resolves resource serializer class
|
91
|
+
#
|
92
|
+
# @return [Class]
|
93
|
+
def self.serializer_class(resource)
|
94
|
+
klass = resource.class
|
95
|
+
klass = resource.first.class if self.is_collection?(resource)
|
95
96
|
|
96
|
-
|
97
|
+
"#{klass.name}Serializer".constantize
|
98
|
+
end
|
97
99
|
end
|
98
100
|
end
|
data/lib/jsonapi/version.rb
CHANGED