jsonapi.rb 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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